fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow (#1816)

* fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow

The thread workflow's CLOSE and RESUME branches called frontmatter.set with
the pre-1.6 fully-positional shape (frontmatter.set <file> <field> <value>).
Since 1.6 the dispatcher (gsd-tools.cjs) parses the file positionally and
reads field/value from the named flags --field/--value via parseNamedArgs;
the positional form leaves field/value undefined, cmdFrontmatterSet errors
'file, field, and value required', and the status/updated writes are
silently skipped. Closing a thread never marked it status: resolved and
resuming never marked it status: in_progress.

Switch all four sites (CLOSE status+updated, RESUME status+updated) to the
1.6 hybrid form that verify-work.md already uses:
  frontmatter.set <file> --field <field> --value <value>

Add a regression test with three guards: (1) behavioral — the named-flag
form writes the field while the positional form errors with the documented
message and does not mutate the file; (2) workflow parity — no workflow
under gsd-core/workflows/ emits the positional form, so a future edit that
reintroduces it anywhere fails CI; (3) thread-specific — CLOSE writes
status: resolved and RESUME writes status: in_progress via the named flags.

* docs(#1778): add changeset fragment for thread workflow frontmatter fix

* docs(#1778): fix unclosed inline-code backtick in changeset fragment

* fix(#1778): move regression into owning test + regen baselines

lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the
#1778 regression (behavioral named-vs-positional + workflow-parity scan +
thread CLOSE/RESUME assertions) into tests/frontmatter-cli.test.cjs, the
canonical home for frontmatter CLI regressions, and delete the standalone
file. frontmatter-cli.test.cjs already carries the allow-test-rule exemption
for workflow .md content tests.

gsd-core/workflows/thread.md ships to every runtime and is size-tracked, so
recapture the 16 golden-install-parity fixtures (thread.md hash) and the
per-file workflow size baseline (thread.md 12400 -> 12464) via UPDATE_GOLDEN=1
and npm run size:baseline.
This commit is contained in:
Tom Boucher
2026-06-28 22:42:44 -04:00
committed by GitHub
parent fd576528a7
commit ac001be49e
20 changed files with 112 additions and 22 deletions

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/spike.md": "0f9a81bcf4573195",
"gsd-core/workflows/stats.md": "a20eb078d2ab11be",
"gsd-core/workflows/sync-skills.md": "8326a7ff0411b077",
"gsd-core/workflows/thread.md": "03a527a71b8fab12",
"gsd-core/workflows/thread.md": "3fb6b552e45fedbd",
"gsd-core/workflows/transition.md": "a7a5fe4040084308",
"gsd-core/workflows/ui-phase.md": "652785fbba26e80c",
"gsd-core/workflows/ui-review.md": "816b2bde136157f9",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "53127654e77256bf",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "c26ca43fdf928d46",
"gsd-core/workflows/thread.md": "6d075b5d26500e9d",
"gsd-core/workflows/transition.md": "eee3435817fab185",
"gsd-core/workflows/ui-phase.md": "e81783508b8b6819",
"gsd-core/workflows/ui-review.md": "1ad3654435000881",

View File

@@ -288,7 +288,7 @@
"gsd-core/workflows/spike.md": "aae8bcad15642645",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "75df5cc71f72c33d",
"gsd-core/workflows/thread.md": "5ad0e0d5ce7e0d11",
"gsd-core/workflows/transition.md": "96ce39403ca69594",
"gsd-core/workflows/ui-phase.md": "790e5982e5b715c3",
"gsd-core/workflows/ui-review.md": "51945fda8e931f99",

View File

@@ -292,7 +292,7 @@
"gsd-core/workflows/spike.md": "204e742c846ee0d9",
"gsd-core/workflows/stats.md": "5cfea82b894eee3c",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "5ae4c3141bdedd88",
"gsd-core/workflows/thread.md": "4a009fd2cc4387a7",
"gsd-core/workflows/transition.md": "fe82e77df8dceb1b",
"gsd-core/workflows/ui-phase.md": "06f1f80de620a319",
"gsd-core/workflows/ui-review.md": "0af83311f5e41f48",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "0051a7e2193a7522",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "c26ca43fdf928d46",
"gsd-core/workflows/thread.md": "6d075b5d26500e9d",
"gsd-core/workflows/transition.md": "eee3435817fab185",
"gsd-core/workflows/ui-phase.md": "93ea0d0dfcb2a1e8",
"gsd-core/workflows/ui-review.md": "1ad3654435000881",

View File

@@ -324,7 +324,7 @@
"gsd-core/workflows/spike.md": "c2f115f0d3251654",
"gsd-core/workflows/stats.md": "0d7449acf349feec",
"gsd-core/workflows/sync-skills.md": "e2b793963799f8ce",
"gsd-core/workflows/thread.md": "d3f768ce0f4b4a4d",
"gsd-core/workflows/thread.md": "ee872031abd592e0",
"gsd-core/workflows/transition.md": "c4bded570fafe712",
"gsd-core/workflows/ui-phase.md": "b373c964b222324c",
"gsd-core/workflows/ui-review.md": "a9b2cbba80482cd8",

View File

@@ -290,7 +290,7 @@
"gsd-core/workflows/spike.md": "716d74cdb2e39a3e",
"gsd-core/workflows/stats.md": "49085df6d4793df3",
"gsd-core/workflows/sync-skills.md": "eca50ffe8320dba8",
"gsd-core/workflows/thread.md": "4c44f10d41740f1d",
"gsd-core/workflows/thread.md": "d53698a91bbbe33c",
"gsd-core/workflows/transition.md": "724b6e9b34d85f26",
"gsd-core/workflows/ui-phase.md": "9fefa0db49f2aa3f",
"gsd-core/workflows/ui-review.md": "731bca05f9770a86",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "9e37f8067adf87b7",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "06e31fdaf02ccabc",
"gsd-core/workflows/thread.md": "cfc42471f9001ba8",
"gsd-core/workflows/transition.md": "96ce39403ca69594",
"gsd-core/workflows/ui-phase.md": "dea2f2d43a43e0d0",
"gsd-core/workflows/ui-review.md": "6b16a7f7783be471",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "c9482514e665bcac",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "c26ca43fdf928d46",
"gsd-core/workflows/thread.md": "6d075b5d26500e9d",
"gsd-core/workflows/transition.md": "69143573741c52e4",
"gsd-core/workflows/ui-phase.md": "bb5167948032872e",
"gsd-core/workflows/ui-review.md": "d256bec482e67af8",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/spike.md": "80844a3c05339fdb",
"gsd-core/workflows/stats.md": "01289f444b66913d",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "7af5046e18c81ee9",
"gsd-core/workflows/thread.md": "596bf37dcea2ce1e",
"gsd-core/workflows/transition.md": "adab3f53afd5d8aa",
"gsd-core/workflows/ui-phase.md": "dd213d91b5c258a5",
"gsd-core/workflows/ui-review.md": "83d6d3b1f26597ff",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "a782dd863771fe0a",
"gsd-core/workflows/stats.md": "17b4f2059f4b4ef2",
"gsd-core/workflows/sync-skills.md": "dc7b8b015afa4b3b",
"gsd-core/workflows/thread.md": "75df5cc71f72c33d",
"gsd-core/workflows/thread.md": "5ad0e0d5ce7e0d11",
"gsd-core/workflows/transition.md": "9040a76741f12de6",
"gsd-core/workflows/ui-phase.md": "33f7113e91c2bfe0",
"gsd-core/workflows/ui-review.md": "013272816a291305",

View File

@@ -325,7 +325,7 @@
"gsd-core/workflows/spike.md": "be2295fe2b32956f",
"gsd-core/workflows/stats.md": "01c24349370a0e6d",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "c26ca43fdf928d46",
"gsd-core/workflows/thread.md": "6d075b5d26500e9d",
"gsd-core/workflows/transition.md": "eee3435817fab185",
"gsd-core/workflows/ui-phase.md": "3dfb9f5161375035",
"gsd-core/workflows/ui-review.md": "1ad3654435000881",

View File

@@ -358,7 +358,7 @@
"gsd-core/workflows/spike.md": "df52b9f31a72d204",
"gsd-core/workflows/stats.md": "598b1bb510ed0451",
"gsd-core/workflows/sync-skills.md": "7e2c138cdbef4282",
"gsd-core/workflows/thread.md": "c11265f0fa0a95d3",
"gsd-core/workflows/thread.md": "f4f8312f744c9cba",
"gsd-core/workflows/transition.md": "2d6739f730c3ea10",
"gsd-core/workflows/ui-phase.md": "38dac814d2d03d6f",
"gsd-core/workflows/ui-review.md": "3d972d3bd30527b3",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/spike.md": "52fcd95f7b343232",
"gsd-core/workflows/stats.md": "7ffa072290ebcae3",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "d85a53d03a3167e0",
"gsd-core/workflows/thread.md": "c10177767648ea7f",
"gsd-core/workflows/transition.md": "1f0a1478d75d89ec",
"gsd-core/workflows/ui-phase.md": "1c91323daf451053",
"gsd-core/workflows/ui-review.md": "f1f82d8257e910cc",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/spike.md": "48df8b626cbd378d",
"gsd-core/workflows/stats.md": "18089135bc41f1b4",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "9fe3ec12e491bec3",
"gsd-core/workflows/thread.md": "61e0eee731434ea9",
"gsd-core/workflows/transition.md": "b49ddd9247f804e7",
"gsd-core/workflows/ui-phase.md": "a725ebdd4f47fb97",
"gsd-core/workflows/ui-review.md": "b3221cac976daffa",

View File

@@ -289,7 +289,7 @@
"gsd-core/workflows/spike.md": "aa5934044317ba7a",
"gsd-core/workflows/stats.md": "c49d3de15375d04b",
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "5ad6ddb308909770",
"gsd-core/workflows/thread.md": "3e9fcfbb254a31a7",
"gsd-core/workflows/transition.md": "9a49f9c48805f666",
"gsd-core/workflows/ui-phase.md": "54e01f1973450a3c",
"gsd-core/workflows/ui-review.md": "fea93c281767f8d7",

View File

@@ -18,7 +18,7 @@ const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { runGsdTools } = require('./helpers.cjs');
const { runGsdTools, parseFrontmatter } = require('./helpers.cjs');
// Track temp files for cleanup
let tempFiles = [];
@@ -441,3 +441,88 @@ describe('Bug #1660: frontmatter set of an object-list field fails closed instea
assert.ok(!parsed.error, 'an idempotent scalar-array set must not produce an error');
});
});
// ─── #1778: thread workflow must use the 1.6 named-flag frontmatter.set form ─
//
// The thread workflow's CLOSE and RESUME branches previously invoked the
// pre-1.6 positional shape (frontmatter.set <file> <field> <value>). Since 1.6
// the dispatcher (gsd-tools.cjs) reads field/value from the named --field/
// --value flags via parseNamedArgs; the positional form leaves field/value
// undefined, cmdFrontmatterSet errors `file, field, and value required`, and
// the status/updated writes are silently skipped — so closing a thread never
// marked it status: resolved and resuming never marked it status: in_progress.
describe('#1778: thread workflow uses the 1.6 named-flag frontmatter.set form', () => {
test('behavioral: named-flag form writes the field; positional form errors and does not mutate', () => {
// 1.6 named-flag form — must succeed and write status: resolved.
const goodFile = writeTempFile('---\nstatus: open\nupdated: "2025-01-01"\n---\n\n# thread body\n');
const good = runGsdTools(['frontmatter', 'set', goodFile, '--field', 'status', '--value', 'resolved']);
assert.ok(good.success, `named-flag form must succeed; stderr: ${good.error}`);
assert.strictEqual(
parseFrontmatter(fs.readFileSync(goodFile, 'utf-8')).status,
'resolved',
'named-flag form must write status: resolved into the file',
);
// Pre-1.6 positional form — must fail with the documented message and NOT mutate.
const badFile = writeTempFile('---\nstatus: open\nupdated: "2025-01-01"\n---\n\n# thread body\n');
const bad = runGsdTools(['frontmatter', 'set', badFile, 'status', 'resolved']);
assert.ok(!bad.success, 'positional form must fail (it is the bug being guarded against)');
assert.ok(
(bad.error + bad.output).includes('file, field, and value required'),
`positional form must error with the documented message; got:\n${bad.error}${bad.output}`,
);
assert.strictEqual(
parseFrontmatter(fs.readFileSync(badFile, 'utf-8')).status,
'open',
'positional form must NOT mutate the file (the silent-failure bug)',
);
});
test('workflow parity: no gsd-core/workflows/*.md emits the positional frontmatter.set form', () => {
const workflowsDir = path.join(__dirname, '..', 'gsd-core', 'workflows');
const files = fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
assert.ok(files.length > 0, 'expected at least one workflow under gsd-core/workflows/');
const offenders = [];
for (const name of files) {
const full = path.join(workflowsDir, name);
const lines = fs.readFileSync(full, 'utf-8').split(/\r?\n/);
lines.forEach((line, i) => {
// Match any frontmatter.set invocation (dot or space form, with or
// without the `gsd_run query` prefix). The 1.6 contract requires
// --field AND --value on every set call; a set line missing --field
// is the pre-1.6 positional form (#1778).
if (!/frontmatter[.\s]+set\b/.test(line)) return;
if (!/--field\b/.test(line) || !/--value\b/.test(line)) {
offenders.push(`${name}:${i + 1}: ${line.trim()}`);
}
});
}
assert.deepStrictEqual(
offenders,
[],
`These workflow frontmatter.set invocations are missing the 1.6 --field/--value named flags (the #1778 positional-form bug):\n ${offenders.join('\n ')}\n\nUse: gsd_run query frontmatter.set <file> --field <field> --value <value>`,
);
});
test('thread workflow CLOSE writes status: resolved and RESUME writes status: in_progress via named flags', () => {
const src = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'workflows', 'thread.md'), 'utf-8');
// CLOSE mode: status resolved + updated, both via named flags.
assert.ok(
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+status\s+--value\s+resolved\b/.test(src),
'CLOSE mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field status --value resolved',
);
assert.ok(
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+updated\s+--value\s+YYYY-MM-DD\b/.test(src),
'CLOSE mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field updated --value YYYY-MM-DD',
);
// RESUME mode: status in_progress + updated, both via named flags.
assert.ok(
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+status\s+--value\s+in_progress\b/.test(src),
'RESUME mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field status --value in_progress',
);
});
});

View File

@@ -78,7 +78,7 @@
"spike.md": 24517,
"stats.md": 6718,
"sync-skills.md": 6125,
"thread.md": 12400,
"thread.md": 12464,
"transition.md": 22016,
"ui-phase.md": 15477,
"ui-review.md": 11172,