@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.738160b9

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 (213) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +17 -4
  3. package/dist/headless-answers.js +1 -1
  4. package/dist/headless-ui.d.ts +1 -1
  5. package/dist/headless-ui.js +1 -1
  6. package/dist/headless.js +1 -1
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  9. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  10. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-budget.js +5 -1
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  14. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  15. package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
  16. package/dist/resources/extensions/gsd/auto-verification.js +5 -2
  17. package/dist/resources/extensions/gsd/auto.js +19 -10
  18. package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
  20. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
  22. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
  23. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
  24. package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  26. package/dist/resources/extensions/gsd/commands-usage.js +1 -1
  27. package/dist/resources/extensions/gsd/db/engine.js +4 -1
  28. package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
  29. package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  31. package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
  32. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
  33. package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
  34. package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
  35. package/dist/resources/extensions/gsd/git-service.js +57 -0
  36. package/dist/resources/extensions/gsd/gitignore.js +4 -1
  37. package/dist/resources/extensions/gsd/json-persistence.js +20 -0
  38. package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
  39. package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
  40. package/dist/resources/extensions/gsd/metrics.js +4 -4
  41. package/dist/resources/extensions/gsd/migrate-external.js +11 -2
  42. package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  45. package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
  46. package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
  47. package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
  48. package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
  49. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
  50. package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
  51. package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
  52. package/dist/resources/extensions/gsd/undo.js +43 -6
  53. package/dist/resources/extensions/gsd/unit-registry.js +4 -2
  54. package/dist/resources/extensions/gsd/verification-gate.js +7 -0
  55. package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  61. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.html +1 -1
  78. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  85. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  86. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  88. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  89. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  90. package/package.json +1 -1
  91. package/packages/cloud-mcp-gateway/package.json +2 -2
  92. package/packages/contracts/package.json +1 -1
  93. package/packages/daemon/package.json +4 -4
  94. package/packages/gsd-agent-core/package.json +5 -5
  95. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
  96. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
  98. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  99. package/packages/gsd-agent-modes/package.json +7 -7
  100. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  101. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  102. package/packages/mcp-server/package.json +4 -4
  103. package/packages/native/package.json +1 -1
  104. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  105. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  106. package/packages/pi-agent-core/package.json +1 -1
  107. package/packages/pi-ai/dist/models.generated.d.ts +46 -0
  108. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  109. package/packages/pi-ai/dist/models.generated.js +38 -0
  110. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  111. package/packages/pi-ai/package.json +1 -1
  112. package/packages/pi-coding-agent/package.json +7 -7
  113. package/packages/pi-tui/package.json +2 -2
  114. package/packages/rpc-client/package.json +2 -2
  115. package/pkg/package.json +1 -1
  116. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  117. package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
  118. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +9 -0
  120. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
  121. package/src/resources/extensions/gsd/auto-budget.ts +12 -1
  122. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  124. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  125. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  126. package/src/resources/extensions/gsd/auto-verification.ts +5 -1
  127. package/src/resources/extensions/gsd/auto.ts +23 -8
  128. package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
  129. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  131. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
  132. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
  133. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
  134. package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
  135. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  136. package/src/resources/extensions/gsd/commands-usage.ts +1 -1
  137. package/src/resources/extensions/gsd/db/engine.ts +8 -1
  138. package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
  139. package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
  140. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  141. package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
  142. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
  143. package/src/resources/extensions/gsd/doctor-types.ts +3 -0
  144. package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
  145. package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
  146. package/src/resources/extensions/gsd/git-service.ts +64 -0
  147. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  148. package/src/resources/extensions/gsd/json-persistence.ts +20 -0
  149. package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
  150. package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
  151. package/src/resources/extensions/gsd/metrics.ts +4 -4
  152. package/src/resources/extensions/gsd/migrate-external.ts +8 -2
  153. package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
  154. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  156. package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
  157. package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
  158. package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
  159. package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
  160. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
  161. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
  162. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
  163. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
  164. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
  165. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
  166. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
  167. package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
  168. package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
  169. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
  170. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
  171. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
  172. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
  173. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
  174. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
  175. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
  176. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
  177. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
  178. package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
  179. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
  180. package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
  181. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
  182. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
  183. package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
  184. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
  185. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
  186. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
  187. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
  188. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
  189. package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
  190. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
  191. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
  193. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
  194. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
  195. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
  196. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
  197. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
  198. package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
  199. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
  200. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
  201. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
  202. package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
  205. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  206. package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
  207. package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
  208. package/src/resources/extensions/gsd/undo.ts +47 -6
  209. package/src/resources/extensions/gsd/unit-registry.ts +4 -2
  210. package/src/resources/extensions/gsd/verification-gate.ts +8 -0
  211. package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
  212. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
  213. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync
4
4
  import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
7
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getTask } from '../gsd-db.ts';
7
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getSlice, getTask, getGateResults } from '../gsd-db.ts';
8
8
  import { handlePlanTask } from '../tools/plan-task.ts';
9
9
  import { parseTaskPlanFile } from '../files.ts';
10
10
 
@@ -65,6 +65,25 @@ test('handlePlanTask writes planning state and renders task plan', async () => {
65
65
  }
66
66
  });
67
67
 
68
+ test('handlePlanTask seeds execute-task gate rows for incremental planning', async () => {
69
+ const base = makeTmpBase();
70
+ openDatabase(join(base, '.gsd', 'gsd.db'));
71
+
72
+ try {
73
+ seedParent();
74
+ const result = await handlePlanTask(validParams(), base);
75
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
76
+
77
+ const gateIds = getGateResults('M001', 'S02', 'task')
78
+ .filter((gate) => gate.task_id === 'T02')
79
+ .map((gate) => gate.gate_id)
80
+ .sort();
81
+ assert.deepEqual(gateIds, ['Q5', 'Q6', 'Q7']);
82
+ } finally {
83
+ cleanup(base);
84
+ }
85
+ });
86
+
68
87
  test('handlePlanTask rejects invalid payloads', async () => {
69
88
  const base = makeTmpBase();
70
89
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -170,6 +189,30 @@ test('handlePlanTask surfaces render failures without changing parse-visible tas
170
189
  }
171
190
  });
172
191
 
192
+ test('handlePlanTask keeps the sketch flag set when the flat-phase slice plan sync fails (#1083)', async () => {
193
+ const base = makeTmpBase();
194
+ openDatabase(join(base, '.gsd', 'gsd.db'));
195
+
196
+ try {
197
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
198
+ insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.', isSketch: true });
199
+
200
+ // The per-task render writes T02-PLAN.md and succeeds, but the flat-phase
201
+ // slice re-render (the canonical PLAN.md that embeds task checkboxes) targets
202
+ // phases/01-test/01-02-PLAN.md. Pre-creating that path as a directory forces
203
+ // EISDIR on the slice sync, so the canonical slice plan never reflects the
204
+ // task. The sketch flag must therefore stay set — the slice keeps refining —
205
+ // instead of leaving the slice out of refining over a stale plan.
206
+ mkdirSync(join(base, '.gsd', 'phases', '01-test', '01-02-PLAN.md'), { recursive: true });
207
+
208
+ const result = await handlePlanTask(validParams(), base);
209
+ assert.equal(getSlice('M001', 'S02')?.is_sketch, 1, 'sketch flag must stay set when the canonical slice plan could not sync');
210
+ assert.ok(getTask('M001', 'S02', 'T02'), 'the task itself is still persisted so the slice can re-sync on retry');
211
+ } finally {
212
+ cleanup(base);
213
+ }
214
+ });
215
+
173
216
  test('handlePlanTask reruns idempotently and refreshes parse-visible state', async () => {
174
217
  const base = makeTmpBase();
175
218
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -10,7 +10,7 @@ import * as os from 'node:os';
10
10
  import { openDatabase, closeDatabase } from '../gsd-db.ts';
11
11
  import { handleCompleteTask } from '../tools/complete-task.ts';
12
12
  import { readEvents } from '../workflow-events.ts';
13
- import { readManifest } from '../workflow-manifest.ts';
13
+ import { flushManifest, readManifest } from '../workflow-manifest.ts';
14
14
 
15
15
  function tempDir(): string {
16
16
  return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-post-hook-'));
@@ -115,8 +115,9 @@ test('post-mutation-hook: state-manifest.json exists after handleCompleteTask',
115
115
  const result = await handleCompleteTask(makeCompleteTaskParams(), base);
116
116
  assert.ok(!('error' in result), `handler should succeed, got: ${JSON.stringify(result)}`);
117
117
 
118
+ await flushManifest(base);
118
119
  const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
119
- assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist after handler completes');
120
+ assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist after manifest flush');
120
121
  } finally {
121
122
  closeDatabase();
122
123
  cleanupDir(base);
@@ -131,6 +132,7 @@ test('post-mutation-hook: manifest has version 1 and includes completed task', a
131
132
 
132
133
  try {
133
134
  await handleCompleteTask(makeCompleteTaskParams(), base);
135
+ await flushManifest(base);
134
136
 
135
137
  const manifest = readManifest(base);
136
138
  assert.ok(manifest !== null, 'manifest should be readable');
@@ -72,6 +72,25 @@ function setupDependencyFixture(
72
72
  mkdirSync(targetSliceDir, { recursive: true });
73
73
  }
74
74
 
75
+ function setupFlatPhaseSlicePlan(base: string): void {
76
+ const phaseDir = join(base, ".gsd", "phases", "01-flat-phase");
77
+ mkdirSync(phaseDir, { recursive: true });
78
+ writeFileSync(
79
+ join(phaseDir, "01-01-PLAN.md"),
80
+ [
81
+ "# S01: Flat phase slice",
82
+ "",
83
+ "## Tasks",
84
+ "",
85
+ "- [ ] **T01: Flat task** `est:15m`",
86
+ "",
87
+ "## Verification",
88
+ "",
89
+ "- Rendered prompt points at this slice plan.",
90
+ ].join("\n"),
91
+ );
92
+ }
93
+
75
94
  // ─── inlineDependencySummaries truncation ─────────────────────────────────────
76
95
 
77
96
  describe("prompt-budget: inlineDependencySummaries truncation", () => {
@@ -412,6 +431,26 @@ describe("prompt-budget: execute-task template", () => {
412
431
  }
413
432
  });
414
433
 
434
+ it("flat-phase execute-task fallback points at the slice plan", async () => {
435
+ const base = createFixtureBase();
436
+ try {
437
+ setupFlatPhaseSlicePlan(base);
438
+
439
+ const prompt = await buildExecuteTaskPrompt("M001", "S01", "Slice", "T01", "Task", base, {
440
+ level: "minimal",
441
+ sessionContextWindow: 128_000,
442
+ });
443
+
444
+ assert.match(
445
+ prompt,
446
+ /Task plan not found at dispatch time\. Read `\.gsd\/phases\/01-flat-phase\/01-01-PLAN\.md` before executing\./,
447
+ );
448
+ assert.doesNotMatch(prompt, /\.gsd\/phases\/01-flat-phase\/tasks\/T01-PLAN\.md/);
449
+ } finally {
450
+ cleanup(base);
451
+ }
452
+ });
453
+
415
454
  it("verificationBudget format varies with context window size", () => {
416
455
  const budget128K = computeBudgets(128_000);
417
456
  const budget1M = computeBudgets(1_000_000);
@@ -566,6 +605,25 @@ describe("prompt-budget: execute-task builder truncation pattern", () => {
566
605
  });
567
606
 
568
607
  describe("prompt-budget: reactive-execute builder", () => {
608
+ it("flat-phase subagent fallback points at the slice plan", async () => {
609
+ const base = createFixtureBase();
610
+ try {
611
+ setupFlatPhaseSlicePlan(base);
612
+
613
+ const prompt = await buildReactiveExecutePrompt("M001", "Milestone", "S01", "Slice", ["T01"], base, undefined, {
614
+ sessionContextWindow: 128_000,
615
+ });
616
+
617
+ assert.match(
618
+ prompt,
619
+ /Task plan not found at dispatch time\. Read `\.gsd\/phases\/01-flat-phase\/01-01-PLAN\.md` before executing\./,
620
+ );
621
+ assert.doesNotMatch(prompt, /\.gsd\/phases\/01-flat-phase\/tasks\/T01-PLAN\.md/);
622
+ } finally {
623
+ cleanup(base);
624
+ }
625
+ });
626
+
569
627
  it("caps dependency carry-forward per ready subagent", async () => {
570
628
  const base = createFixtureBase();
571
629
  try {
@@ -533,6 +533,7 @@ test("plan-milestone prompt references DB-backed planning tool and explicitly fo
533
533
  assert.match(prompt, /Do \*\*not\*\* write `?\{\{outputPath\}\}`?, `?ROADMAP\.md`?, or other planning artifacts manually/i);
534
534
  assert.match(prompt, /NEVER call `gsd_plan_milestone` with only `milestoneId` and `sliceId`/);
535
535
  assert.match(prompt, /gsd_plan_slice/);
536
+ assert.match(prompt, /gsd_plan_task/);
536
537
  });
537
538
 
538
539
  test("discuss prompts forbid gsd_plan_milestone slice-only tool args", () => {
@@ -548,12 +549,12 @@ test("plan-slice prompt no longer frames direct PLAN writes as the source of tru
548
549
  assert.match(prompt, /Do \*\*not\*\* rely on direct `PLAN\.md` writes as the source of truth/i);
549
550
  });
550
551
 
551
- test("plan-slice prompt explicitly names gsd_plan_slice as DB-backed planning tool", () => {
552
+ test("plan-slice prompt explicitly names incremental DB-backed planning tools", () => {
552
553
  const prompt = readPrompt("plan-slice");
553
554
  assert.match(prompt, /gsd_plan_slice/);
554
555
  assert.match(prompt, /gsd_plan_task/);
555
- // The prompt should describe the DB-backed tool as the canonical write path
556
- assert.match(prompt, /DB-backed tool is the canonical write path/i);
556
+ assert.match(prompt, /Persist incrementally through DB-backed tools/i);
557
+ assert.match(prompt, /DB-backed tools are the canonical write path/i);
557
558
  });
558
559
 
559
560
  test("plan-slice prompt does not instruct direct file writes as a primary step", () => {
@@ -562,11 +563,12 @@ test("plan-slice prompt does not instruct direct file writes as a primary step",
562
563
  assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{outputPath\}\}`?\s*$/m);
563
564
  });
564
565
 
565
- test("plan-slice prompt clarifies gsd_plan_slice handles task persistence", () => {
566
+ test("plan-slice prompt requires per-task incremental persistence", () => {
566
567
  const prompt = readPrompt("plan-slice");
567
- // gsd_plan_slice persists tasks in its transaction — no separate gsd_plan_task calls needed
568
568
  assert.match(prompt, /gsd_plan_task/);
569
- assert.match(prompt, /gsd_plan_slice` handles task persistence/i);
569
+ assert.match(prompt, /omit `tasks` for this metadata call/i);
570
+ assert.match(prompt, /call `gsd_plan_task` once per task/i);
571
+ assert.doesNotMatch(prompt, /do not call `gsd_plan_task`/i);
570
572
  });
571
573
 
572
574
  test("plan-slice prompt references web app UAT guidance when planning browser work", () => {
@@ -0,0 +1,103 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { autoSession } from "../auto-runtime-state.ts";
5
+ import { registerHooks } from "../bootstrap/register-hooks.ts";
6
+ import { resetToolCallLoopGuard } from "../bootstrap/tool-call-loop-guard.ts";
7
+
8
+ type Handler = (event: any, ctx?: any) => Promise<any> | any;
9
+
10
+ function makeHookHarness(): {
11
+ emitToolCall: (toolName: string, input: Record<string, unknown>) => Promise<any>;
12
+ } {
13
+ const handlers = new Map<string, Handler[]>();
14
+ const pi = {
15
+ on(event: string, handler: Handler) {
16
+ const existing = handlers.get(event) ?? [];
17
+ existing.push(handler);
18
+ handlers.set(event, existing);
19
+ },
20
+ };
21
+ const ctx = {
22
+ cwd: process.cwd(),
23
+ ui: { notify: () => undefined },
24
+ };
25
+ let callId = 0;
26
+
27
+ registerHooks(pi as any, []);
28
+
29
+ return {
30
+ async emitToolCall(toolName: string, input: Record<string, unknown>): Promise<any> {
31
+ callId += 1;
32
+ const loopGuardHandler = handlers.get("tool_call")?.[0];
33
+ assert.ok(loopGuardHandler, "loop-guard tool_call handler should be registered");
34
+ return loopGuardHandler({ toolCallId: `loop-${callId}`, toolName, input }, ctx);
35
+ },
36
+ };
37
+ }
38
+
39
+ test("register-hooks keeps loop-guard block reason interactive outside auto-mode", async (t) => {
40
+ autoSession.reset();
41
+ resetToolCallLoopGuard();
42
+ t.after(() => {
43
+ autoSession.reset();
44
+ resetToolCallLoopGuard();
45
+ });
46
+
47
+ const { emitToolCall } = makeHookHarness();
48
+
49
+ for (let i = 0; i < 4; i += 1) {
50
+ await emitToolCall("web_search", { query: "same query" });
51
+ }
52
+ const block = await emitToolCall("web_search", { query: "same query" });
53
+
54
+ assert.equal(block?.block, true);
55
+ assert.match(block.reason, /respond to the user in text/);
56
+ assert.doesNotMatch(block.reason, /auto-mode recovery\/replan path/);
57
+ });
58
+
59
+ test("register-hooks rewrites loop-guard block reason for auto-mode identical-args blocks", async (t) => {
60
+ autoSession.reset();
61
+ resetToolCallLoopGuard();
62
+ autoSession.active = true;
63
+ t.after(() => {
64
+ autoSession.reset();
65
+ resetToolCallLoopGuard();
66
+ });
67
+
68
+ const { emitToolCall } = makeHookHarness();
69
+
70
+ for (let i = 0; i < 4; i += 1) {
71
+ await emitToolCall("web_search", { query: "same query" });
72
+ }
73
+ const block = await emitToolCall("web_search", { query: "same query" });
74
+
75
+ assert.equal(block?.block, true);
76
+ assert.match(block.reason, /Tool loop detected \(identical args\): web_search/);
77
+ assert.match(block.reason, /Do not re-issue this blocked tool/);
78
+ assert.match(block.reason, /auto-mode recovery\/replan path/);
79
+ assert.doesNotMatch(block.reason, /respond to the user in text/);
80
+ });
81
+
82
+ test("register-hooks rewrites loop-guard block reason for auto-mode per-tool blocks", async (t) => {
83
+ autoSession.reset();
84
+ resetToolCallLoopGuard();
85
+ autoSession.active = true;
86
+ t.after(() => {
87
+ autoSession.reset();
88
+ resetToolCallLoopGuard();
89
+ });
90
+
91
+ const { emitToolCall } = makeHookHarness();
92
+
93
+ for (let i = 0; i < 6; i += 1) {
94
+ await emitToolCall("gsd_complete_milestone", { milestone: `M${i}` });
95
+ }
96
+ const block = await emitToolCall("gsd_complete_milestone", { milestone: "M7" });
97
+
98
+ assert.equal(block?.block, true);
99
+ assert.match(block.reason, /Tool loop detected \(repeated tool\): gsd_complete_milestone/);
100
+ assert.match(block.reason, /Do not re-issue this blocked tool/);
101
+ assert.match(block.reason, /auto-mode recovery\/replan path/);
102
+ assert.doesNotMatch(block.reason, /respond to the user in text/);
103
+ });
@@ -7,7 +7,26 @@ import { closeSync, ftruncateSync, openSync, statSync, writeFileSync } from "nod
7
7
  import { join } from "node:path";
8
8
 
9
9
  import { captureRootDirtySnapshot } from "../root-write-leak-guard.ts";
10
- import { cleanup, git, makeTempRepo } from "./test-utils.ts";
10
+ import { cleanup, createFile, git, makeTempRepo } from "./test-utils.ts";
11
+
12
+ test("captureRootDirtySnapshot ignores unignored GSD root runtime directories", () => {
13
+ const base = makeTempRepo("gsd-root-dirty-runtime-");
14
+ try {
15
+ createFile(base, ".gsd/STATE.md", "state\n");
16
+ createFile(base, ".gsd-backups/snap-001", "backup\n");
17
+ createFile(base, ".gsd-worktrees/M001/file.txt", "worktree\n");
18
+ createFile(base, "src/foo.ts", "export {};\n");
19
+
20
+ const snapshot = captureRootDirtySnapshot(base);
21
+
22
+ assert.equal(snapshot.has(".gsd/STATE.md"), false);
23
+ assert.equal(snapshot.has(".gsd-backups/snap-001"), false);
24
+ assert.equal(snapshot.has(".gsd-worktrees/M001/file.txt"), false);
25
+ assert.equal(snapshot.has("src/foo.ts"), true);
26
+ } finally {
27
+ cleanup(base);
28
+ }
29
+ });
11
30
 
12
31
  test("captureRootDirtySnapshot does not read dirty files larger than Node's Buffer limit", () => {
13
32
  const base = makeTempRepo("gsd-root-dirty-large-");
@@ -184,10 +184,15 @@ test("auto Unit tool scope allows reactive-execute status and diagnostic tools",
184
184
  }
185
185
  });
186
186
 
187
- test("auto Unit tool scope blocks stale per-task planner in slice planning phases", () => {
188
- for (const unitType of ["plan-slice", "refine-slice", "replan-slice"]) {
187
+ test("auto Unit tool scope allows incremental task planning during plan-slice", () => {
188
+ const result = shouldBlockAutoUnitToolCall("plan-slice", "gsd_plan_task");
189
+ assert.equal(result.block, false, "plan-slice should be able to call gsd_plan_task incrementally");
190
+ });
191
+
192
+ test("auto Unit tool scope still blocks per-task planner outside plan-slice", () => {
193
+ for (const unitType of ["refine-slice", "replan-slice"]) {
189
194
  const result = shouldBlockAutoUnitToolCall(unitType, "gsd_plan_task");
190
- assert.equal(result.block, true, `${unitType} should not call stale gsd_plan_task`);
195
+ assert.equal(result.block, true, `${unitType} should not call gsd_plan_task`);
191
196
  }
192
197
  });
193
198
 
@@ -7,6 +7,7 @@ import { test } from 'node:test';
7
7
  import assert from 'node:assert/strict';
8
8
  import {
9
9
  checkToolCallLoop,
10
+ recordToolCallLoopMutation,
10
11
  resetToolCallLoopGuard,
11
12
  disableToolCallLoopGuard,
12
13
  getToolCallLoopCount,
@@ -223,6 +224,62 @@ console.log('\n── Loop guard: repeatable tools get the higher cap (#783) ─
223
224
  assert.ok(blocked.reason!.includes('cap 15'), 'reason should mention the repeatable cap');
224
225
  }
225
226
 
227
+ // ═══════════════════════════════════════════════════════════════════════════
228
+ // Mutation progress decays per-tool counts (#1092)
229
+ // ═══════════════════════════════════════════════════════════════════════════
230
+
231
+ console.log('\n── Loop guard: mutation progress decays per-tool counts (#1092) ──');
232
+
233
+ {
234
+ resetToolCallLoopGuard();
235
+
236
+ // bash is called more than the repeatable cap, but each count is separated
237
+ // by a successful file mutation. Guard 2 should treat that as progress and
238
+ // avoid blocking the progressing unit.
239
+ for (let i = 1; i <= 20; i++) {
240
+ const result = checkToolCallLoop('bash', { command: `echo ${i}` });
241
+ assert.ok(result.block === false, `bash call ${i} after mutation progress should be allowed`);
242
+ assert.deepStrictEqual(getToolCallCountForTool('bash'), 1, `bash count should decay before call ${i}`);
243
+
244
+ const mutationTool = i % 2 === 0 ? 'write' : 'edit';
245
+ const mutation = checkToolCallLoop(mutationTool, {
246
+ path: `file-${i}.ts`,
247
+ content: `content ${i}`,
248
+ });
249
+ assert.ok(mutation.block === false, `${mutationTool} mutation ${i} should be allowed`);
250
+ recordToolCallLoopMutation(mutationTool);
251
+ }
252
+ }
253
+
254
+ // ═══════════════════════════════════════════════════════════════════════════
255
+ // Distinct read-only navigation calls are context gathering, not repeated-tool loops.
256
+ // ═══════════════════════════════════════════════════════════════════════════
257
+
258
+ console.log('\n── Loop guard: distinct read-only navigation calls are not capped by per-tool count ──');
259
+
260
+ {
261
+ const exemptTools = ['find', 'glob', 'grep', 'ls', 'read', 'search_and_read'];
262
+
263
+ for (const toolName of exemptTools) {
264
+ resetToolCallLoopGuard();
265
+
266
+ for (let i = 1; i <= 20; i++) {
267
+ const result = checkToolCallLoop(toolName, { path: `file-${i}.ts` });
268
+ assert.ok(result.block === false, `distinct ${toolName} call ${i} should be allowed`);
269
+ }
270
+
271
+ resetToolCallLoopGuard();
272
+ for (let i = 1; i <= 4; i++) {
273
+ const result = checkToolCallLoop(toolName, { path: 'same-file.ts' });
274
+ assert.ok(result.block === false, `identical ${toolName} call ${i} should be allowed`);
275
+ }
276
+
277
+ const blocked = checkToolCallLoop(toolName, { path: 'same-file.ts' });
278
+ assert.ok(blocked.block === true, `5th identical ${toolName} call should still be blocked`);
279
+ assert.ok(blocked.reason?.includes('identical args'), `${toolName} loops should be caught by Guard 1`);
280
+ }
281
+ }
282
+
226
283
  // ═══════════════════════════════════════════════════════════════════════════
227
284
  // Per-tool counts are independent per tool and reset together
228
285
  // ═══════════════════════════════════════════════════════════════════════════
@@ -245,15 +302,15 @@ console.log('\n── Loop guard: per-tool counts are independent and reset toge
245
302
  }
246
303
 
247
304
  // ═══════════════════════════════════════════════════════════════════════════
248
- // Newly-repeatable tools from core-session-tools.ts get the higher cap.
249
- // bg_shell, find, ls, search_and_read were added to INHERENTLY_REPEATABLE_TOOL_SET
250
- // in the code-quality consolidation PR (previously only bash/read/write/etc).
305
+ // Non-exempt repeatable tools from core-session-tools.ts get the higher cap.
306
+ // Read-only navigation tools are exempted earlier; mutating/execution repeatable
307
+ // tools (e.g. gsd_exec) must still trip the per-tool cap.
251
308
  // ═══════════════════════════════════════════════════════════════════════════
252
309
 
253
- console.log('\n── Loop guard: new repeatable tools (bg_shell, find, ls, search_and_read) get high cap ──');
310
+ console.log('\n── Loop guard: non-exempt repeatable tools get high cap ──');
254
311
 
255
312
  {
256
- for (const toolName of ['bg_shell', 'find', 'ls', 'search_and_read']) {
313
+ for (const toolName of ['bg_shell', 'gsd_exec', 'write']) {
257
314
  resetToolCallLoopGuard();
258
315
 
259
316
  // Should allow up to 15 varied calls without blocking.
@@ -385,14 +385,17 @@ test("gsd_plan_slice — enrichment fields are optional", () => {
385
385
 
386
386
  const required = new Set(getRequiredProps(tool));
387
387
 
388
- // Core fields
389
- const coreRequired = ["milestoneId", "sliceId", "goal", "tasks"];
388
+ // Core fields. `tasks` is intentionally NOT here: incremental planning
389
+ // (#1027) lets gsd_plan_slice persist slice metadata only, then add tasks
390
+ // one at a time via gsd_plan_task, so tasks must be optional.
391
+ const coreRequired = ["milestoneId", "sliceId", "goal"];
390
392
  for (const field of coreRequired) {
391
393
  assert.ok(required.has(field), `core field "${field}" must be required`);
392
394
  }
393
395
 
394
- // Enrichment fields
396
+ // Enrichment fields (plus tasks, optional for incremental planning).
395
397
  const enrichmentFields = [
398
+ "tasks",
396
399
  "successCriteria",
397
400
  "proofLevel",
398
401
  "integrationClosure",
@@ -10,6 +10,7 @@ import {
10
10
  handleUndo,
11
11
  handleUndoTask,
12
12
  handleResetSlice,
13
+ parseActivityLogFilename,
13
14
  uncheckTaskInPlan,
14
15
  } from "../undo.ts";
15
16
  import {
@@ -67,6 +68,37 @@ test("handleUndo without --force only warns and leaves completed units intact",
67
68
  }
68
69
  });
69
70
 
71
+ test("handleUndo execute-task with --force reopens the task row and re-renders plan", async () => {
72
+ const base = makeTempDir("gsd-undo-execute-task");
73
+ try {
74
+ setupTaskFixture(base);
75
+ mkdirSync(join(base, ".gsd", "activity"), { recursive: true });
76
+ writeFileSync(
77
+ join(base, ".gsd", "activity", "001-execute-task-M001-S01-T01.jsonl"),
78
+ "",
79
+ "utf-8",
80
+ );
81
+
82
+ const { notifications, ctx } = makeCtx();
83
+ await handleUndo("--force", ctx, {} as any, base);
84
+
85
+ const task = getTask("M001", "S01", "T01");
86
+ assert.equal(task?.status, "pending");
87
+
88
+ const planContent = readFileSync(
89
+ join(base, ".gsd", "phases", "01-test", "01-01-PLAN.md"),
90
+ "utf-8",
91
+ );
92
+ assert.match(planContent, /\[ \] \*\*T01\*\*:/);
93
+
94
+ assert.equal(notifications[0]?.level, "success");
95
+ assert.match(notifications[0]?.message ?? "", /Unchecked task in PLAN/);
96
+ } finally {
97
+ closeDatabase();
98
+ rmSync(base, { recursive: true, force: true });
99
+ }
100
+ });
101
+
70
102
  test("uncheckTaskInPlan flips a checked task back to unchecked", () => {
71
103
  const base = makeTempDir("gsd-undo-plan");
72
104
  try {
@@ -154,6 +186,48 @@ test("extractCommitShas ignores malformed commit tokens", () => {
154
186
  assert.deepEqual(extractCommitShas(content), ["1234567"]);
155
187
  });
156
188
 
189
+ test("parseActivityLogFilename splits hyphenated unit types from their IDs", () => {
190
+ // Task-level: unit type and ID both contain hyphens.
191
+ assert.deepEqual(parseActivityLogFilename("001-execute-task-M001-S01-T01.jsonl"), {
192
+ unitType: "execute-task",
193
+ unitId: "M001-S01-T01",
194
+ });
195
+ // Variant must win over its shorter prefix ("execute-task").
196
+ assert.deepEqual(parseActivityLogFilename("002-execute-task-simple-M001-S02-T03.jsonl"), {
197
+ unitType: "execute-task-simple",
198
+ unitId: "M001-S02-T03",
199
+ });
200
+ // Milestone- and slice-shaped IDs.
201
+ assert.deepEqual(parseActivityLogFilename("003-research-milestone-M001.jsonl"), {
202
+ unitType: "research-milestone",
203
+ unitId: "M001",
204
+ });
205
+ assert.deepEqual(parseActivityLogFilename("004-plan-slice-M001-S01.jsonl"), {
206
+ unitType: "plan-slice",
207
+ unitId: "M001-S01",
208
+ });
209
+ });
210
+
211
+ test("parseActivityLogFilename accepts non-milestone-shaped unit IDs", () => {
212
+ // Regression (#1057): project-level units use IDs that do not start with
213
+ // "M<digit>". A milestone-shaped regex made /gsd undo bail out with
214
+ // "could not parse latest activity log" when one of these was most recent.
215
+ assert.deepEqual(parseActivityLogFilename("005-discuss-project-PROJECT.jsonl"), {
216
+ unitType: "discuss-project",
217
+ unitId: "PROJECT",
218
+ });
219
+ assert.deepEqual(parseActivityLogFilename("006-workflow-preferences-WORKFLOW-PREFS.jsonl"), {
220
+ unitType: "workflow-preferences",
221
+ unitId: "WORKFLOW-PREFS",
222
+ });
223
+ });
224
+
225
+ test("parseActivityLogFilename returns null for unrecognized names", () => {
226
+ assert.equal(parseActivityLogFilename("007-not-a-real-unit-M001.jsonl"), null);
227
+ assert.equal(parseActivityLogFilename("no-sequence-prefix.jsonl"), null);
228
+ assert.equal(parseActivityLogFilename("001-execute-task-M001-S01-T01.txt"), null);
229
+ });
230
+
157
231
  // ─── handleUndoTask tests ────────────────────────────────────────────────────
158
232
 
159
233
  function makeCtx(): { notifications: Array<{ message: string; level: string }>; ctx: any } {
@@ -18,6 +18,7 @@ import { join } from "node:path";
18
18
  import { execSync } from "node:child_process";
19
19
  import { GIT_NO_PROMPT_ENV } from "../git-constants.ts";
20
20
  import { handleTurnGitActionError, runTurnGitAction } from "../git-service.ts";
21
+ import { _resetLogs, drainLogs } from "../workflow-logger.ts";
21
22
 
22
23
  function run(cmd: string, cwd: string): string {
23
24
  return execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
@@ -111,6 +112,43 @@ workspace:
111
112
  }
112
113
  });
113
114
 
115
+ test("uok gitops turn action status-only reports dirty undeclared nested git repositories", () => {
116
+ const root = mkdtempSync(join(tmpdir(), "gsd-uok-gitops-nested-"));
117
+ try {
118
+ initRepo(root);
119
+ mkdirSync(join(root, "nested"), { recursive: true });
120
+ initRepo(join(root, "nested"));
121
+ writeFileSync(join(root, ".gitignore"), "nested/\n", "utf-8");
122
+ run("git add .gitignore", root);
123
+ run('git commit -m "chore: ignore nested repo"', root);
124
+
125
+ writeFileSync(join(root, "nested", "README.md"), "# Dirty nested\n", "utf-8");
126
+
127
+ _resetLogs();
128
+ const result = runTurnGitAction({
129
+ basePath: root,
130
+ action: "status-only",
131
+ unitType: "execute-task",
132
+ unitId: "M001/S01/T01",
133
+ });
134
+ const warnings = drainLogs();
135
+
136
+ assert.equal(result.status, "ok");
137
+ assert.equal(result.dirty, true);
138
+ assert.deepEqual(result.dirtyRepositories, {
139
+ project: false,
140
+ "nested (undeclared)": true,
141
+ });
142
+ assert.ok(
143
+ warnings.some((entry) => entry.message.includes("undeclared dirty nested git repo nested")),
144
+ "dirty undeclared nested repo should be logged as a warning",
145
+ );
146
+ } finally {
147
+ _resetLogs();
148
+ rmSync(root, { recursive: true, force: true });
149
+ }
150
+ });
151
+
114
152
  test("uok gitops turn action snapshot writes snapshot refs", () => {
115
153
  const repo = makeRepo();
116
154
  try {