@opengsd/gsd-core 1.5.0-rc.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +1 -1
  3. package/agents/gsd-executor.md +22 -0
  4. package/agents/gsd-phase-researcher.md +1 -1
  5. package/agents/gsd-planner.md +4 -0
  6. package/agents/gsd-project-researcher.md +1 -1
  7. package/agents/gsd-verifier.md +45 -13
  8. package/bin/install.js +56 -100
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/execute-phase.md +1 -1
  11. package/commands/gsd/plan-phase.md +1 -1
  12. package/gemini-extension.json +1 -1
  13. package/gsd-core/bin/gsd-tools.cjs +85 -53
  14. package/gsd-core/bin/lib/agent-command-router.cjs +2 -2
  15. package/gsd-core/bin/lib/agent-install-check.cjs +143 -0
  16. package/gsd-core/bin/lib/audit-command-router.cjs +4 -4
  17. package/gsd-core/bin/lib/capability-activation.cjs +37 -10
  18. package/gsd-core/bin/lib/capability-registry.cjs +2 -0
  19. package/gsd-core/bin/lib/capability-state.cjs +80 -30
  20. package/gsd-core/bin/lib/capability-writer.cjs +5 -4
  21. package/gsd-core/bin/lib/check-command-router.cjs +50 -51
  22. package/gsd-core/bin/lib/commands.cjs +20 -2
  23. package/gsd-core/bin/lib/config-loader.cjs +3 -4
  24. package/gsd-core/bin/lib/config-schema.cjs +1 -1
  25. package/gsd-core/bin/lib/config-types.cjs +2 -1
  26. package/gsd-core/bin/lib/config.cjs +5 -2
  27. package/gsd-core/bin/lib/decisions.cjs +19 -1
  28. package/gsd-core/bin/lib/docs.cjs +14 -2
  29. package/gsd-core/bin/lib/frontmatter.cjs +2 -2
  30. package/gsd-core/bin/lib/gap-checker.cjs +5 -2
  31. package/gsd-core/bin/lib/git-base-branch.cjs +27 -1
  32. package/gsd-core/bin/lib/graphify-command-router.cjs +6 -8
  33. package/gsd-core/bin/lib/graphify.cjs +7 -31
  34. package/gsd-core/bin/lib/gsd2-import.cjs +2 -2
  35. package/gsd-core/bin/lib/init.cjs +30 -4
  36. package/gsd-core/bin/lib/intel-command-router.cjs +6 -3
  37. package/gsd-core/bin/lib/intel.cjs +28 -34
  38. package/gsd-core/bin/lib/io.cjs +2 -4
  39. package/gsd-core/bin/lib/learnings.cjs +2 -2
  40. package/gsd-core/bin/lib/loop-resolver.cjs +45 -167
  41. package/gsd-core/bin/lib/milestone.cjs +13 -5
  42. package/gsd-core/bin/lib/model-resolver.cjs +3 -4
  43. package/gsd-core/bin/lib/phase-id.cjs +3 -5
  44. package/gsd-core/bin/lib/phase-locator.cjs +3 -6
  45. package/gsd-core/bin/lib/phase.cjs +59 -13
  46. package/gsd-core/bin/lib/probe-core.cjs +40 -11
  47. package/gsd-core/bin/lib/profile-output.cjs +5 -2
  48. package/gsd-core/bin/lib/prohibition-enforcement.cjs +660 -0
  49. package/gsd-core/bin/lib/roadmap-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/roadmap-parser.cjs +28 -25
  51. package/gsd-core/bin/lib/roadmap.cjs +9 -4
  52. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
  53. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
  54. package/gsd-core/bin/lib/state.cjs +75 -17
  55. package/gsd-core/bin/lib/task-command-router.cjs +2 -2
  56. package/gsd-core/bin/lib/teams-status.cjs +74 -0
  57. package/gsd-core/bin/lib/template.cjs +11 -2
  58. package/gsd-core/bin/lib/uat.cjs +64 -2
  59. package/gsd-core/bin/lib/verification.cjs +8 -5
  60. package/gsd-core/bin/lib/verify.cjs +311 -4
  61. package/gsd-core/bin/lib/workstream-inventory.cjs +2 -2
  62. package/gsd-core/bin/lib/workstream.cjs +8 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +44 -3
  64. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  65. package/gsd-core/references/planner-antipatterns.md +46 -0
  66. package/gsd-core/references/planning-config.md +5 -1
  67. package/gsd-core/references/prohibition-probe.md +80 -2
  68. package/gsd-core/references/worktree-branch-check.md +11 -5
  69. package/gsd-core/templates/verification-report.md +16 -3
  70. package/gsd-core/workflows/docs-update.md +23 -31
  71. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
  72. package/gsd-core/workflows/execute-phase.md +7 -2
  73. package/gsd-core/workflows/map-codebase.md +8 -10
  74. package/gsd-core/workflows/plan-phase.md +6 -0
  75. package/gsd-core/workflows/quick.md +26 -2
  76. package/gsd-core/workflows/settings-advanced.md +5 -5
  77. package/gsd-core/workflows/settings-integrations.md +5 -5
  78. package/gsd-core/workflows/spec-phase.md +30 -2
  79. package/gsd-core/workflows/verify-phase.md +22 -7
  80. package/hooks/dist/gsd-worktree-path-guard.js +34 -18
  81. package/hooks/gsd-worktree-path-guard.js +34 -18
  82. package/package.json +3 -3
  83. package/scripts/ci-prepare-test-scope.cjs +56 -14
  84. package/scripts/diff-touches-shipped-paths.cjs +5 -11
  85. package/scripts/gen-capability-registry.cjs +27 -1
  86. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  87. package/scripts/research-profiles.cjs +2 -2
  88. package/scripts/run-tests.cjs +1 -0
  89. package/gsd-core/bin/lib/core.cjs +0 -345
@@ -22,8 +22,8 @@ const roadmapUpgrade = require("./roadmap-upgrade.cjs");
22
22
  const planningWorkspace = require("./planning-workspace.cjs");
23
23
  const { planningDir } = planningWorkspace;
24
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
- const core = require("./core.cjs");
26
- const { loadConfig } = core;
25
+ const configLoaderMod = require("./config-loader.cjs");
26
+ const { loadConfig } = configLoaderMod;
27
27
  // ─── W021 Implementation ──────────────────────────────────────────────────────
28
28
  /**
29
29
  * Check each phase entry in a milestone-prefixed ROADMAP.md for W021 violations.
@@ -6,12 +6,10 @@
6
6
  * Owns shipped-milestone slicing, current-milestone extraction,
7
7
  * milestone/phase lookups, and milestone-phase filtering.
8
8
  * Behaviour is preserved byte-for-behaviour from the prior location;
9
- * only the module boundary moved. core.cjs re-exports every symbol here
10
- * under its own `export =` object so existing consumers are unaffected.
9
+ * only the module boundary moved. The core.cjs re-export spine was retired
10
+ * in epic #1267; callers import roadmap-parser helpers directly.
11
11
  *
12
- * New imports should pull roadmap-parser helpers from roadmap-parser.cjs directly.
13
- *
14
- * Dependencies (leaf modules only — no core.cjs, no loadConfig):
12
+ * Dependencies (leaf modules only — no loadConfig):
15
13
  * - node:fs / node:path (stdlib)
16
14
  * - ./phase-id.cjs (escapeRegex, phaseMarkdownRegexSource)
17
15
  * - ./planning-workspace.cjs (planningDir)
@@ -180,6 +178,27 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
180
178
  const after = content.slice(offset);
181
179
  return before + after.replace(pattern, replacement);
182
180
  }
181
+ function findRoadmapPhaseInContent(content, phaseNum) {
182
+ const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
183
+ const headerMatch = content.match(phasePattern);
184
+ if (!headerMatch)
185
+ return null;
186
+ const phaseName = headerMatch[1].trim();
187
+ const headerIndex = headerMatch.index;
188
+ const restOfContent = content.slice(headerIndex);
189
+ const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i);
190
+ const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
191
+ const section = content.slice(headerIndex, sectionEnd).trim();
192
+ const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
193
+ const goal = goalMatch ? goalMatch[1].trim() : null;
194
+ return {
195
+ found: true,
196
+ phase_number: String(phaseNum),
197
+ phase_name: phaseName,
198
+ goal,
199
+ section,
200
+ };
201
+ }
183
202
  function getRoadmapPhaseInternal(cwd, phaseNum) {
184
203
  if (!phaseNum)
185
204
  return null;
@@ -191,26 +210,10 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
191
210
  if (roadmapRaw === null)
192
211
  throw new Error('missing');
193
212
  const content = extractCurrentMilestone(roadmapRaw, cwd);
194
- const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
195
- const headerMatch = content.match(phasePattern);
196
- if (!headerMatch)
197
- return null;
198
- const phaseName = headerMatch[1].trim();
199
- const headerIndex = headerMatch.index;
200
- const restOfContent = content.slice(headerIndex);
201
- const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i);
202
- const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
203
- const section = content.slice(headerIndex, sectionEnd).trim();
204
- const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
205
- const goal = goalMatch ? goalMatch[1].trim() : null;
206
- return {
207
- found: true,
208
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
209
- phase_number: String(phaseNum),
210
- phase_name: phaseName,
211
- goal,
212
- section,
213
- };
213
+ const scopedResult = findRoadmapPhaseInContent(content, phaseNum);
214
+ if (scopedResult)
215
+ return scopedResult;
216
+ return findRoadmapPhaseInContent(stripShippedMilestones(roadmapRaw), phaseNum);
214
217
  }
215
218
  catch {
216
219
  return null;
@@ -13,8 +13,14 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const clock_cjs_1 = require("./clock.cjs");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
- const core = require("./core.cjs");
17
- const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, phaseTokenMatches } = core;
16
+ const ioMod = require("./io.cjs");
17
+ const { output, error } = ioMod;
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const phaseIdMod = require("./phase-id.cjs");
20
+ const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, phaseTokenMatches } = phaseIdMod;
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const phaseLocatorMod = require("./phase-locator.cjs");
23
+ const { findPhaseInternal } = phaseLocatorMod;
18
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
25
  const roadmapParserModule = require("./roadmap-parser.cjs");
20
26
  const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
@@ -77,8 +83,7 @@ function countPhasePlansAndSummaries(phaseDir) {
77
83
  hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
78
84
  };
79
85
  }
80
- // `phaseMarkdownRegexSource` moved to core.cjs (#3537) so phase.cjs and
81
- // core.cjs itself can consume it without circular deps. Imported above.
86
+ // `phaseMarkdownRegexSource` lives in phase-id.cjs (#3537) and is imported above.
82
87
  // ─── searchPhaseInContent ─────────────────────────────────────────────────────
83
88
  /**
84
89
  * Search for a phase header (and its section) within the given content string.
@@ -287,6 +287,9 @@ function skillFrontmatterName(skillDirName) {
287
287
  // Return the hyphen form as-is (gsd-<cmd>) — canonical since #2808.
288
288
  return skillDirName;
289
289
  }
290
+ function normalizeClaudeSkillEffort(effort) {
291
+ return effort === 'xhigh' ? 'max' : effort;
292
+ }
290
293
  /**
291
294
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
292
295
  * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -386,7 +389,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
386
389
  if (context)
387
390
  fm += `context: ${context}\n`;
388
391
  if (effort)
389
- fm += `effort: ${effort}\n`;
392
+ fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
390
393
  if (toolsBlock)
391
394
  fm += toolsBlock;
392
395
  fm += '---';
@@ -406,7 +406,25 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
406
406
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
407
407
  parsed = {};
408
408
  const usesNestedHooksObject = parsed['hooks'] && typeof parsed['hooks'] === 'object' && !Array.isArray(parsed['hooks']);
409
- const hookTable = usesNestedHooksObject ? parsed['hooks'] : parsed;
409
+ // #1348: canonicalize every write to the nested { hooks: { <Event>: [...] } }
410
+ // shape. Lift ANY top-level event array (legacy, empty, OR mixed nested+top-level)
411
+ // into the nested table — merging when the same event exists in both — so
412
+ // user/legacy entries are preserved under `hooks` and no stray top-level event
413
+ // key survives (Codex deny_unknown_fields rejects them). Mirrors reconcileCursorHooksJson.
414
+ const hookTable = usesNestedHooksObject
415
+ ? parsed['hooks']
416
+ : {};
417
+ for (const key of Object.keys(parsed)) {
418
+ if (key === 'hooks')
419
+ continue;
420
+ if (Array.isArray(parsed[key])) {
421
+ const lifted = parsed[key];
422
+ const existing = Array.isArray(hookTable[key]) ? hookTable[key] : [];
423
+ hookTable[key] = [...lifted, ...existing];
424
+ delete parsed[key];
425
+ }
426
+ }
427
+ parsed['hooks'] = hookTable;
410
428
  const eventEntries = Array.isArray(hookTable[eventName]) ? hookTable[eventName] : [];
411
429
  let removedLegacy = false;
412
430
  const sanitizedEntries = [];
@@ -452,8 +470,11 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
452
470
  else {
453
471
  delete hookTable[eventName];
454
472
  }
455
- if (usesNestedHooksObject)
456
- parsed['hooks'] = hookTable;
473
+ // Avoid writing an empty `{ "hooks": {} }` artifact (e.g. removal on an absent
474
+ // file): collapse an empty hook table back to `{}` so the existing
475
+ // shouldWrite/no-write-on-empty behavior is preserved.
476
+ if (Object.keys(hookTable).length === 0)
477
+ delete parsed['hooks'];
457
478
  const nextContent = `${JSON.stringify(parsed, null, 2)}\n`;
458
479
  const changed = currentContent !== nextContent;
459
480
  const shouldWrite = changed && (currentContent !== null || Object.keys(parsed).length > 0);
@@ -12,8 +12,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
- const core = require("./core.cjs");
16
- const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, extractCurrentMilestone, output, error } = core;
15
+ const ioMod = require("./io.cjs");
16
+ const { output, error } = ioMod;
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const configLoaderMod = require("./config-loader.cjs");
19
+ const { loadConfig } = configLoaderMod;
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const phaseIdMod = require("./phase-id.cjs");
22
+ const { escapeRegex } = phaseIdMod;
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const roadmapParserMod = require("./roadmap-parser.cjs");
25
+ const { getMilestoneInfo, getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
17
26
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
28
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -25,6 +34,19 @@ const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
25
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
26
35
  const scanPhasePlans = require("./plan-scan.cjs");
27
36
  const state_document_cjs_1 = require("./state-document.cjs");
37
+ const STATE_PROGRESS_RESYNC_FIELDS = new Set([
38
+ 'Progress',
39
+ 'Total Plans in Phase',
40
+ 'Total Phases',
41
+ ]);
42
+ function shouldResyncStateProgress(fields) {
43
+ for (const field of fields) {
44
+ if (STATE_PROGRESS_RESYNC_FIELDS.has(field)) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
28
50
  // ─── Cache ────────────────────────────────────────────────────────────────────
29
51
  // Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
30
52
  // Avoids re-reading N+1 directories on every state write when the phase structure
@@ -148,6 +170,7 @@ function cmdStatePatch(cwd, patches, raw) {
148
170
  const statePath = planningPaths(cwd).state;
149
171
  try {
150
172
  const results = { updated: [], failed: [] };
173
+ const shouldResync = shouldResyncStateProgress(Object.keys(patches));
151
174
  // Use atomic read-modify-write to prevent lost updates from concurrent agents
152
175
  readModifyWriteStateMd(statePath, (content) => {
153
176
  for (const [field, value] of Object.entries(patches)) {
@@ -161,7 +184,7 @@ function cmdStatePatch(cwd, patches, raw) {
161
184
  }
162
185
  }
163
186
  return content;
164
- }, cwd);
187
+ }, cwd, { resync: shouldResync });
165
188
  output(results, raw, results.updated.length > 0 ? 'true' : 'false');
166
189
  }
167
190
  catch {
@@ -182,7 +205,7 @@ function cmdStateUpdate(cwd, field, value) {
182
205
  const statePath = planningPaths(cwd).state;
183
206
  try {
184
207
  let updated = false;
185
- const shouldResync = ['Progress', 'Total Plans in Phase', 'Total Phases'].includes(field);
208
+ const shouldResync = shouldResyncStateProgress([field]);
186
209
  // Preserve curated progress for body-only updates, but allow fields that
187
210
  // directly project into progress.* frontmatter to rebuild after mutation.
188
211
  readModifyWriteStateMd(statePath, (content) => {
@@ -922,6 +945,32 @@ function matchSessionSection(body) {
922
945
  return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i)
923
946
  || body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i);
924
947
  }
948
+ function parseProsePhaseField(value) {
949
+ if (!value)
950
+ return { phase: null, name: null };
951
+ const phaseMatch = value.match(/\b(\d+[A-Z]?(?:\.\d+)*)\b/i);
952
+ const parenName = value.match(/\(([^)]+)\)/);
953
+ const dashName = value.match(/—\s*([^(\n]+?)(?:\s*\(|$)/);
954
+ const rawName = parenName?.[1] ?? dashName?.[1] ?? null;
955
+ const name = rawName && !/^(?:complete|executing|not started)$/i.test(rawName.trim())
956
+ ? rawName.trim()
957
+ : null;
958
+ return {
959
+ phase: phaseMatch ? phaseMatch[1] : null,
960
+ name,
961
+ };
962
+ }
963
+ function parseProseLastActivityField(value) {
964
+ if (!value)
965
+ return { date: null, description: null };
966
+ const match = value.match(/^(\d{4}-\d{2}-\d{2})(?:\s+[—-]{1,2}\s+(.+))?$/);
967
+ if (!match)
968
+ return { date: value, description: null };
969
+ return {
970
+ date: match[1],
971
+ description: match[2]?.trim() || null,
972
+ };
973
+ }
925
974
  function cmdStateSnapshot(cwd, raw) {
926
975
  const statePath = planningPaths(cwd).state;
927
976
  if (!node_fs_1.default.existsSync(statePath)) {
@@ -950,15 +999,18 @@ function cmdStateSnapshot(cwd, raw) {
950
999
  return null;
951
1000
  };
952
1001
  // Extract basic fields — frontmatter keys take precedence over body
953
- const currentPhase = fmScalar('current_phase') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase');
954
- const currentPhaseName = fmScalar('current_phase_name') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase Name');
1002
+ const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(body, 'Phase'));
1003
+ const currentPhase = fmScalar('current_phase') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') ?? prosePhase.phase;
1004
+ const currentPhaseName = fmScalar('current_phase_name') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase Name') ?? prosePhase.name;
955
1005
  const totalPhasesRaw = fmScalar('total_phases') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Total Phases');
956
1006
  const currentPlan = fmScalar('current_plan') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Plan');
957
1007
  const totalPlansRaw = fmScalar('total_plans_in_phase') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Total Plans in Phase');
958
1008
  const status = fmScalar('status') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Status');
959
1009
  const progressRaw = fmScalar('progress') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Progress');
960
- const lastActivity = fmScalar('last_activity') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Last Activity');
961
- const lastActivityDesc = fmScalar('last_activity_desc') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Last Activity Description');
1010
+ const rawLastActivity = (0, state_document_cjs_1.stateExtractField)(body, 'Last Activity') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Last activity');
1011
+ const proseLastActivity = parseProseLastActivityField(rawLastActivity);
1012
+ const lastActivity = fmScalar('last_activity') ?? proseLastActivity.date ?? rawLastActivity;
1013
+ const lastActivityDesc = fmScalar('last_activity_desc') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Last Activity Description') ?? proseLastActivity.description;
962
1014
  const pausedAt = fmScalar('paused_at') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Paused At');
963
1015
  // Parse numeric fields
964
1016
  const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
@@ -1043,14 +1095,18 @@ function cmdStateSnapshot(cwd, raw) {
1043
1095
  * reliably via `state json` instead of fragile regex parsing.
1044
1096
  */
1045
1097
  function buildStateFrontmatter(bodyContent, cwd) {
1046
- const currentPhase = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase');
1047
- const currentPhaseName = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase Name');
1098
+ const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(bodyContent, 'Phase'));
1099
+ const currentPhase = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase') ?? prosePhase.phase;
1100
+ const currentPhaseName = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase Name') ?? prosePhase.name;
1048
1101
  const currentPlan = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Plan');
1049
1102
  const totalPhasesRaw = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Total Phases');
1050
1103
  const totalPlansRaw = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Total Plans in Phase');
1051
1104
  const status = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Status');
1052
1105
  const progressRaw = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Progress');
1053
- const lastActivity = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Last Activity');
1106
+ const rawLastActivity = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Last Activity') ?? (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Last activity');
1107
+ const proseLastActivity = parseProseLastActivityField(rawLastActivity);
1108
+ const lastActivity = proseLastActivity.date ?? rawLastActivity;
1109
+ const lastActivityDesc = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Last Activity Description') ?? proseLastActivity.description;
1054
1110
  // Bug #2444: scope Stopped At extraction to the ## Session section so that
1055
1111
  // historical "Stopped at:" prose elsewhere in the body (e.g. in a
1056
1112
  // Session Continuity Archive section) never overwrites the current value.
@@ -1194,6 +1250,8 @@ function buildStateFrontmatter(bodyContent, cwd) {
1194
1250
  fm['last_updated'] = clock_cjs_1.realClock.nowIso();
1195
1251
  if (lastActivity)
1196
1252
  fm['last_activity'] = lastActivity;
1253
+ if (lastActivityDesc)
1254
+ fm['last_activity_desc'] = lastActivityDesc;
1197
1255
  const progress = {};
1198
1256
  if (totalPhases !== null)
1199
1257
  progress['total_phases'] = totalPhases;
@@ -1745,14 +1803,14 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
1745
1803
  posBody = replaced;
1746
1804
  }
1747
1805
  // Update Last activity line if present
1748
- const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
1806
+ const newActivity = `Last activity: ${today} — Phase ${phaseNumber} execution started`;
1749
1807
  if (/^Last activity:/im.test(posBody)) {
1750
1808
  posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
1751
1809
  }
1752
1810
  else {
1753
1811
  // Pipe-table format in Current Position (#1255)
1754
1812
  // Value must match the inline branch (date + narrative), not bare date.
1755
- const activityValue = `${today} -- Phase ${phaseNumber} execution started`;
1813
+ const activityValue = `${today} — Phase ${phaseNumber} execution started`;
1756
1814
  const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', activityValue)
1757
1815
  ?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', activityValue);
1758
1816
  if (replaced !== null)
@@ -1770,7 +1828,7 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
1770
1828
  if (positionMatch) {
1771
1829
  const header = positionMatch[1];
1772
1830
  let posBody = positionMatch[2];
1773
- const resumeActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution resumed (wave continue)`;
1831
+ const resumeActivity = `Last activity: ${today} — Phase ${phaseNumber} execution resumed (wave continue)`;
1774
1832
  if (/^Last activity:/im.test(posBody)) {
1775
1833
  posBody = posBody.replace(/^Last activity:.*$/im, resumeActivity);
1776
1834
  body = body.replace(positionPattern, () => `${header}${posBody}`);
@@ -1933,7 +1991,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
1933
1991
  // Update Current Position section
1934
1992
  body = updateCurrentPositionFields(body, {
1935
1993
  status: 'Ready to execute',
1936
- lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
1994
+ lastActivity: `${today} — Phase ${phaseNumber} planning complete`,
1937
1995
  });
1938
1996
  return reassemble(body);
1939
1997
  }, cwd, { resync: false });
@@ -2472,14 +2530,14 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
2472
2530
  posBody = replaced;
2473
2531
  }
2474
2532
  // Update Last activity line if present
2475
- const newActivity = `Last activity: ${today} -- Phase ${currentPhase} marked complete`;
2533
+ const newActivity = `Last activity: ${today} — Phase ${currentPhase} marked complete`;
2476
2534
  if (/^Last activity:/im.test(posBody)) {
2477
2535
  posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
2478
2536
  }
2479
2537
  else {
2480
2538
  // Pipe-table format in Current Position (#1255)
2481
2539
  // Value must match the inline branch (date + narrative), not bare date.
2482
- const activityValue = `${today} -- Phase ${currentPhase} marked complete`;
2540
+ const activityValue = `${today} — Phase ${currentPhase} marked complete`;
2483
2541
  const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', activityValue)
2484
2542
  ?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', activityValue);
2485
2543
  if (replaced !== null)
@@ -12,8 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
- const core = require("./core.cjs");
16
- const { output, error, ERROR_REASON } = core;
15
+ const ioMod = require("./io.cjs");
16
+ const { output, error, ERROR_REASON } = ioMod;
17
17
  // ─── Implementation ───────────────────────────────────────────────────────────
18
18
  function isBehaviorAddingTaskContent(content) {
19
19
  const tddTrue = /\btdd\s*=\s*["']true["']/i.test(content);
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * Teams Status Module — issue #1355
4
+ *
5
+ * Read-only detector for claude-code's experimental agent-teams feature.
6
+ * Exposes a PURE core function (env injected, no process.env/disk inside) and
7
+ * a thin CLI wrapper that reuses resolveRuntime from runtime-slash.cjs.
8
+ *
9
+ * Exports:
10
+ * resolveTeamsStatus({ runtime, env }) → TeamsStatus
11
+ * cmdTeamsStatus(cwd, opts) — I/O entry point
12
+ *
13
+ * resolveTeamsStatus is PURE: env and runtime are injected, no process.env or
14
+ * disk access inside the function. Pass process.env explicitly at call sites.
15
+ *
16
+ * cmdTeamsStatus is the I/O handler. It reads process.env, resolves the
17
+ * runtime via resolveRuntime(cwd) from runtime-slash.cjs (GSD_RUNTIME →
18
+ * config.runtime → 'claude' precedence), then:
19
+ * - default: prints JSON.stringify(status) to stdout via io.output, exits 0.
20
+ * - --active: prints nothing, exits 0 if status.active, exit 1 otherwise.
21
+ *
22
+ * Strictly read-only — no config writes, no disk mutation.
23
+ *
24
+ * Dependencies:
25
+ * - ./io.cjs (output)
26
+ * - ./runtime-slash.cjs (resolveRuntime)
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.resolveTeamsStatus = resolveTeamsStatus;
30
+ exports.cmdTeamsStatus = cmdTeamsStatus;
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const ioMod = require("./io.cjs");
33
+ const { output: coreOutput } = ioMod;
34
+ // ─── Pure core ────────────────────────────────────────────────────────────────
35
+ /**
36
+ * Resolve the agent-teams status from injected runtime and env.
37
+ *
38
+ * Strict truthiness: only '1' and 'true' (case-insensitive, trimmed) are on.
39
+ * '0', 'false', '', and unset are all off.
40
+ *
41
+ * @param opts.runtime The resolved runtime name (e.g. 'claude', 'codex')
42
+ * @param opts.env The environment map to read from (typically process.env)
43
+ */
44
+ function resolveTeamsStatus(opts) {
45
+ const raw = (opts.env['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'] ?? '').trim().toLowerCase();
46
+ const envOn = raw === '1' || raw === 'true'; // strict: never '0'/'false'/'' as on
47
+ const isClaude = opts.runtime === 'claude';
48
+ const source = !isClaude ? 'off: non-claude' : (envOn ? 'on: env' : 'off: flag absent');
49
+ return { active: envOn && isClaude, runtime: opts.runtime, env_present: envOn, source };
50
+ }
51
+ // ─── CLI command handler ──────────────────────────────────────────────────────
52
+ /**
53
+ * Command entry point: resolve runtime via resolveRuntime(cwd), read process.env,
54
+ * call resolveTeamsStatus, and emit the result.
55
+ *
56
+ * @param cwd Project root directory (used by resolveRuntime for config.json)
57
+ * @param opts Command options
58
+ * @param opts.active When true: print nothing, exit 0 if active, exit 1 otherwise
59
+ */
60
+ function cmdTeamsStatus(cwd, opts) {
61
+ // Resolve runtime via the canonical precedence:
62
+ // GSD_RUNTIME → config.runtime → 'claude'
63
+ // Reuses resolveRuntime from runtime-slash.cjs — no reimplementation.
64
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
65
+ const runtimeSlash = require('./runtime-slash.cjs');
66
+ const runtime = runtimeSlash.resolveRuntime(cwd);
67
+ const status = resolveTeamsStatus({ runtime, env: process.env });
68
+ if (opts.active) {
69
+ // --active mode: no output, exit code encodes the boolean
70
+ process.exit(status.active ? 0 : 1);
71
+ }
72
+ // Default: emit JSON to stdout via io.output, exit 0
73
+ coreOutput(status, false);
74
+ }
@@ -12,8 +12,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
- const core = require("./core.cjs");
16
- const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosixPath, output, error } = core;
15
+ const ioMod = require("./io.cjs");
16
+ const { output, error } = ioMod;
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const coreUtilsMod = require("./core-utils.cjs");
19
+ const { toPosixPath, generateSlugInternal } = coreUtilsMod;
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const phaseIdMod = require("./phase-id.cjs");
22
+ const { normalizePhaseName } = phaseIdMod;
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const phaseLocatorMod = require("./phase-locator.cjs");
25
+ const { findPhaseInternal } = phaseLocatorMod;
17
26
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
28
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -15,8 +15,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  const node_fs_1 = __importDefault(require("node:fs"));
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports
18
- const core = require("./core.cjs");
19
- const { output, error, getMilestonePhaseFilter, toPosixPath } = core;
18
+ const io = require("./io.cjs");
19
+ const { output, error } = io;
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const roadmapParser = require("./roadmap-parser.cjs");
22
+ const { getMilestonePhaseFilter } = roadmapParser;
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const coreUtils = require("./core-utils.cjs");
25
+ const { toPosixPath } = coreUtils;
20
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
27
  const planningWorkspace = require("./planning-workspace.cjs");
22
28
  const { planningDir } = planningWorkspace;
@@ -139,6 +145,13 @@ function parseCurrentTest(content) {
139
145
  || section.match(/^expected:\s*\|\n([\s\S]+)/m);
140
146
  const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
141
147
  if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
148
+ if (!numberMatch && !nameMatch && !expectedBlockMatch && !expectedInlineMatch) {
149
+ const pendingTest = parseFirstPendingTest(content);
150
+ if (pendingTest) {
151
+ return pendingTest;
152
+ }
153
+ error('Current Test section is non-structured and no pending UAT test remains to resume');
154
+ }
142
155
  error('Current Test section is malformed');
143
156
  }
144
157
  let expected;
@@ -159,6 +172,55 @@ function parseCurrentTest(content) {
159
172
  expected: (0, security_cjs_1.sanitizeForDisplay)(expected),
160
173
  };
161
174
  }
175
+ function parseFirstPendingTest(content) {
176
+ const testsMatch = content.match(/##\s*Tests\s*\n([\s\S]*?)(?=\n##\s|$)/i);
177
+ if (!testsMatch) {
178
+ return null;
179
+ }
180
+ const testsSection = testsMatch[1];
181
+ const headingPattern = /^###\s*(\d+)\.\s*([^\n]+)\s*$/gm;
182
+ const headings = [];
183
+ let headingMatch;
184
+ while ((headingMatch = headingPattern.exec(testsSection)) !== null) {
185
+ headings.push({
186
+ index: headingMatch.index,
187
+ number: parseInt(headingMatch[1], 10),
188
+ name: headingMatch[2].trim(),
189
+ });
190
+ }
191
+ for (let i = 0; i < headings.length; i += 1) {
192
+ const current = headings[i];
193
+ const next = headings[i + 1];
194
+ const block = testsSection.slice(current.index, next ? next.index : undefined);
195
+ if (!/^result:\s*\[?pending\]?\s*$/im.test(block)) {
196
+ continue;
197
+ }
198
+ const expected = parseExpectedFromTestBlock(block);
199
+ if (!expected) {
200
+ error(`Pending UAT test ${current.number} is missing an expected field`);
201
+ }
202
+ return {
203
+ complete: false,
204
+ number: current.number,
205
+ name: (0, security_cjs_1.sanitizeForDisplay)(current.name),
206
+ expected: (0, security_cjs_1.sanitizeForDisplay)(expected),
207
+ };
208
+ }
209
+ return null;
210
+ }
211
+ function parseExpectedFromTestBlock(block) {
212
+ const expectedBlockMatch = block.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
213
+ || block.match(/^expected:\s*\|\n([\s\S]+)/m);
214
+ if (expectedBlockMatch) {
215
+ return expectedBlockMatch[1]
216
+ .split('\n')
217
+ .map((line) => line.replace(/^ {2}/, ''))
218
+ .join('\n')
219
+ .trim();
220
+ }
221
+ const expectedInlineMatch = block.match(/^expected:\s*(.+)\s*$/m);
222
+ return expectedInlineMatch ? expectedInlineMatch[1].trim() : null;
223
+ }
162
224
  // ─── buildCheckpoint ──────────────────────────────────────────────────────────
163
225
  function buildCheckpoint(currentTest) {
164
226
  return [
@@ -21,11 +21,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  };
22
22
  const node_fs_1 = __importDefault(require("node:fs"));
23
23
  const node_path_1 = __importDefault(require("node:path"));
24
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- core.cjs is an export= CommonJS module
25
- const core = require("./core.cjs");
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
25
+ const io = require("./io.cjs");
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
27
+ const phaseId = require("./phase-id.cjs");
26
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
27
29
  const frontmatterMod = require("./frontmatter.cjs");
28
- const { output, extractPhaseToken } = core;
30
+ const { output, error } = io;
31
+ const { extractPhaseToken } = phaseId;
29
32
  const { extractFrontmatter } = frontmatterMod;
30
33
  // ─── Constants ────────────────────────────────────────────────────────────────
31
34
  /** The set of status values that the gsd-verifier agent emits. */
@@ -170,7 +173,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
170
173
  }
171
174
  /**
172
175
  * CLI command handler: resolve phaseDir against cwd, call readVerificationStatus,
173
- * emit via core.output().
176
+ * emit via io.output().
174
177
  *
175
178
  * @param cwd - Current working directory (used to resolve phaseDirArg).
176
179
  * @param phaseDirArg - Phase directory path (absolute or relative to cwd).
@@ -178,7 +181,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
178
181
  */
179
182
  function cmdVerificationStatus(cwd, phaseDirArg, raw) {
180
183
  if (!phaseDirArg) {
181
- core.error('phase directory required for verification.status');
184
+ error('phase directory required for verification.status');
182
185
  return;
183
186
  }
184
187
  const phaseDir = node_path_1.default.resolve(cwd, phaseDirArg);