From f94aef73039f1ca8f78de0e6be0bf9a5c242c7d5 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 13 May 2026 11:21:50 -0700 Subject: [PATCH] fix(learnings): accept type:"investigation" in gstack-learnings-log The /investigate skill instructed agents to log learnings with type:"investigation", but bin/gstack-learnings-log:22 rejected anything not in [pattern, pitfall, preference, architecture, tool, operational]. Every investigation run exited 1 to stderr and the learning was dropped, silently to the user. Fix: add 'investigation' to ALLOWED_TYPES. Regression test: round-trips a learning with type:"investigation" and asserts exit 0 + file write; second test reads investigate/SKILL.md.tmpl and asserts it emits the literal type:"investigation" string, guarding the template/validator contract at both ends. Fixes #1423. Reported by diogolealassis. --- bin/gstack-learnings-log | 3 ++- test/learnings.test.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bin/gstack-learnings-log b/bin/gstack-learnings-log index 5f53e1902..ad27091e5 100755 --- a/bin/gstack-learnings-log +++ b/bin/gstack-learnings-log @@ -1,6 +1,7 @@ #!/usr/bin/env bash # gstack-learnings-log — append a learning to the project learnings file # Usage: gstack-learnings-log '{"skill":"review","type":"pitfall","key":"n-plus-one","insight":"...","confidence":8,"source":"observed"}' +# Valid types: pattern, pitfall, preference, architecture, tool, operational, investigation # # Append-only storage. Duplicates (same key+type) are resolved at read time # by gstack-learnings-search ("latest winner" per key+type). @@ -19,7 +20,7 @@ let j; try { j = JSON.parse(raw); } catch { process.stderr.write('gstack-learnings-log: invalid JSON, skipping\n'); process.exit(1); } // Field validation: type must be from allowed list -const ALLOWED_TYPES = ['pattern', 'pitfall', 'preference', 'architecture', 'tool', 'operational']; +const ALLOWED_TYPES = ['pattern', 'pitfall', 'preference', 'architecture', 'tool', 'operational', 'investigation']; if (!j.type || !ALLOWED_TYPES.includes(j.type)) { process.stderr.write('gstack-learnings-log: invalid type \"' + (j.type || '') + '\", must be one of: ' + ALLOWED_TYPES.join(', ') + '\n'); process.exit(1); diff --git a/test/learnings.test.ts b/test/learnings.test.ts index 6d72266c4..fc4033a6c 100644 --- a/test/learnings.test.ts +++ b/test/learnings.test.ts @@ -102,6 +102,27 @@ describe('gstack-learnings-log', () => { const lines = fs.readFileSync(f!, 'utf-8').trim().split('\n'); expect(lines.length).toBe(2); }); + + // Regression test for #1423: investigate skill emits type:"investigation" + // but ALLOWED_TYPES previously rejected it. Now accepted. + test('accepts type:"investigation" (regression: #1423)', () => { + const input = '{"skill":"investigate","type":"investigation","key":"root-cause","insight":"verified","confidence":9,"source":"observed"}'; + const result = runLog(input); + expect(result.exitCode).toBe(0); + const f = findLearningsFile(); + expect(f).not.toBeNull(); + const parsed = JSON.parse(fs.readFileSync(f!, 'utf-8').trim()); + expect(parsed.type).toBe('investigation'); + }); + + // Caller contract: investigate/SKILL.md.tmpl must emit type:"investigation" + // verbatim. Guards against the template drifting to an invalid type and + // silently breaking the log path. See codex review finding for #1423. + test('investigate template emits type:"investigation" verbatim (caller contract)', () => { + const tmpl = fs.readFileSync(path.join(ROOT, 'investigate/SKILL.md.tmpl'), 'utf-8'); + // The invocation line must include "type":"investigation" exactly. + expect(tmpl).toContain('"type":"investigation"'); + }); }); describe('gstack-learnings-search', () => {