@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.d2070876

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 (177) hide show
  1. package/dist/headless.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  5. package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
  8. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
  9. package/dist/resources/extensions/gsd/auto-start.js +29 -9
  10. package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
  11. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  12. package/dist/resources/extensions/gsd/auto.js +15 -5
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
  14. package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
  15. package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
  16. package/dist/resources/extensions/gsd/db/engine.js +5 -2
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
  18. package/dist/resources/extensions/gsd/error-classifier.js +12 -2
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
  20. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  21. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  22. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  23. package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
  24. package/dist/resources/extensions/gsd/paths.js +17 -7
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  26. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
  29. package/dist/resources/extensions/gsd/stop-notice.js +9 -0
  30. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  31. package/dist/resources/extensions/shared/interview-ui.js +43 -6
  32. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  61. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  62. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +2 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/gsd-cloud/README.md +9 -3
  75. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  76. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  77. package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
  78. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  79. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  80. package/packages/gsd-cloud/dist/cli.js +121 -15
  81. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  82. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  83. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  84. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  85. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  86. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
  87. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
  89. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  90. package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
  91. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  92. package/packages/gsd-cloud/dist/runtime-process.js +272 -0
  93. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  94. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  95. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  96. package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
  97. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  98. package/packages/gsd-cloud/package.json +2 -2
  99. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  100. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  101. package/packages/mcp-server/dist/cli-runner.js +119 -33
  102. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  103. package/packages/mcp-server/package.json +4 -4
  104. package/packages/native/package.json +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +17 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +17 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
  112. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  113. package/packages/pi-ai/package.json +1 -1
  114. package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
  115. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  117. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  118. package/packages/pi-coding-agent/package.json +7 -7
  119. package/packages/pi-tui/package.json +2 -2
  120. package/packages/rpc-client/package.json +2 -2
  121. package/pkg/package.json +1 -1
  122. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  123. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  124. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  126. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  127. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  128. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  129. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  130. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  131. package/src/resources/extensions/gsd/auto.ts +20 -7
  132. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  133. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  134. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  135. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  136. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  137. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  138. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  139. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  140. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  141. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  143. package/src/resources/extensions/gsd/paths.ts +17 -7
  144. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  145. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  146. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  147. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  148. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  150. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  151. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  152. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  153. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  154. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  155. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  156. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  157. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  158. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  159. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  161. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  162. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  163. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  164. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  165. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  167. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  168. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  169. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  170. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  171. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  172. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  173. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  174. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  175. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  176. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
  177. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
@@ -67,6 +67,13 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
67
67
 
68
68
  import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
69
69
  import { applyStatusTransition } from "./db/writers/status.js";
70
+ import {
71
+ LAYOUT_SEGMENTS,
72
+ derivePhaseSlug,
73
+ milestoneIdToPhaseNum,
74
+ milestoneIdUniqueSuffix,
75
+ phaseDirName,
76
+ } from "./layout-policy.js";
70
77
 
71
78
  export function insertDecision(d: Omit<Decision, "seq">): void {
72
79
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -206,6 +213,47 @@ export function insertArtifact(a: {
206
213
  });
207
214
  }
208
215
 
216
+ function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
217
+ const phaseNum = milestoneIdToPhaseNum(milestoneId);
218
+ const slug = derivePhaseSlug(title || milestoneId);
219
+ const suffix = milestoneIdUniqueSuffix(milestoneId);
220
+ return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
221
+ }
222
+
223
+ function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
224
+ const db = getDbOrNull()!;
225
+ const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
226
+ const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
227
+ const staleRows = db.prepare(
228
+ `SELECT path
229
+ FROM artifacts
230
+ WHERE milestone_id = :milestone_id
231
+ AND path LIKE :phase_prefix`,
232
+ ).all({
233
+ ":milestone_id": milestoneId,
234
+ ":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
235
+ }) as Array<{ path: string }>;
236
+
237
+ const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
238
+ const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
239
+ const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
240
+
241
+ for (const row of staleRows) {
242
+ const parts = row.path.split("/");
243
+ if (parts.length < 3) continue;
244
+ if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
245
+ if (parts[1] === canonicalDir) continue;
246
+ if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
247
+
248
+ const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
249
+ if (existingPath.get({ ":path": newPath })) {
250
+ deletePath.run({ ":path": row.path });
251
+ continue;
252
+ }
253
+ updatePath.run({ ":new_path": newPath, ":old_path": row.path });
254
+ }
255
+ }
256
+
209
257
  export interface MilestonePlanningRecord {
210
258
  vision: string;
211
259
  successCriteria: string[];
@@ -287,39 +335,43 @@ export function insertMilestone(m: {
287
335
 
288
336
  export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
289
337
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
290
- getDbOrNull()!.prepare(
291
- `UPDATE milestones SET
292
- title = COALESCE(NULLIF(:title, ''), title),
293
- status = COALESCE(NULLIF(:status, ''), status),
294
- depends_on = COALESCE(:depends_on, depends_on),
295
- vision = COALESCE(:vision, vision),
296
- success_criteria = COALESCE(:success_criteria, success_criteria),
297
- key_risks = COALESCE(:key_risks, key_risks),
298
- proof_strategy = COALESCE(:proof_strategy, proof_strategy),
299
- verification_contract = COALESCE(:verification_contract, verification_contract),
300
- verification_integration = COALESCE(:verification_integration, verification_integration),
301
- verification_operational = COALESCE(:verification_operational, verification_operational),
302
- verification_uat = COALESCE(:verification_uat, verification_uat),
303
- definition_of_done = COALESCE(:definition_of_done, definition_of_done),
304
- requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
305
- boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
306
- WHERE id = :id`,
307
- ).run({
308
- ":id": milestoneId,
309
- ":title": planning.title ?? "",
310
- ":status": planning.status ?? "",
311
- ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
312
- ":vision": planning.vision ?? null,
313
- ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
314
- ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
315
- ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
316
- ":verification_contract": planning.verificationContract ?? null,
317
- ":verification_integration": planning.verificationIntegration ?? null,
318
- ":verification_operational": planning.verificationOperational ?? null,
319
- ":verification_uat": planning.verificationUat ?? null,
320
- ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
321
- ":requirement_coverage": planning.requirementCoverage ?? null,
322
- ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
338
+ transaction(() => {
339
+ getDbOrNull()!.prepare(
340
+ `UPDATE milestones SET
341
+ title = COALESCE(NULLIF(:title, ''), title),
342
+ status = COALESCE(NULLIF(:status, ''), status),
343
+ depends_on = COALESCE(:depends_on, depends_on),
344
+ vision = COALESCE(:vision, vision),
345
+ success_criteria = COALESCE(:success_criteria, success_criteria),
346
+ key_risks = COALESCE(:key_risks, key_risks),
347
+ proof_strategy = COALESCE(:proof_strategy, proof_strategy),
348
+ verification_contract = COALESCE(:verification_contract, verification_contract),
349
+ verification_integration = COALESCE(:verification_integration, verification_integration),
350
+ verification_operational = COALESCE(:verification_operational, verification_operational),
351
+ verification_uat = COALESCE(:verification_uat, verification_uat),
352
+ definition_of_done = COALESCE(:definition_of_done, definition_of_done),
353
+ requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
354
+ boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
355
+ WHERE id = :id`,
356
+ ).run({
357
+ ":id": milestoneId,
358
+ ":title": planning.title ?? "",
359
+ ":status": planning.status ?? "",
360
+ ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
361
+ ":vision": planning.vision ?? null,
362
+ ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
363
+ ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
364
+ ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
365
+ ":verification_contract": planning.verificationContract ?? null,
366
+ ":verification_integration": planning.verificationIntegration ?? null,
367
+ ":verification_operational": planning.verificationOperational ?? null,
368
+ ":verification_uat": planning.verificationUat ?? null,
369
+ ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
370
+ ":requirement_coverage": planning.requirementCoverage ?? null,
371
+ ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
372
+ });
373
+ const finalTitle = planning.title?.trim();
374
+ if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
323
375
  });
324
376
  }
325
377
 
@@ -1282,7 +1282,7 @@ export async function buildDiscussSlicePrompt(
1282
1282
  : "";
1283
1283
 
1284
1284
  const inlinedTemplates = inlineTemplate("slice-context", "Slice Context");
1285
- return loadPrompt("guided-discuss-slice", {
1285
+ return prependLanguageDirective(base, loadPrompt("guided-discuss-slice", {
1286
1286
  milestoneId: mid,
1287
1287
  sliceId: sid,
1288
1288
  sliceTitle: sTitle,
@@ -1293,7 +1293,7 @@ export async function buildDiscussSlicePrompt(
1293
1293
  inlinedTemplates,
1294
1294
  structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false",
1295
1295
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
1296
- });
1296
+ }));
1297
1297
  }
1298
1298
 
1299
1299
  /**
@@ -12,7 +12,7 @@
12
12
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
13
13
  import { logWarning } from "./workflow-logger.js";
14
14
  import { isClosedStatus } from "./status-guards.js";
15
- import { dirname, join, relative } from "node:path";
15
+ import { dirname, join } from "node:path";
16
16
  import {
17
17
  getAllMilestones,
18
18
  getMilestone,
@@ -50,7 +50,7 @@ import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.j
50
50
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
51
51
  import { invalidateStateCache } from "./state.js";
52
52
  import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
53
- import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
53
+ import { readCompatMarker, writeCompatMarker, computeProjectionSha, deriveCompatProjectionKey } from "./compat/compat-marker.js";
54
54
  import type { RiskLevel } from "./types.js";
55
55
  import {
56
56
  phaseDirName,
@@ -109,13 +109,7 @@ function flushProjectionWritesToMarker(): void {
109
109
  */
110
110
  function toArtifactPath(absPath: string, basePath: string): string {
111
111
  const projectionRoot = gsdProjectionRoot(basePath);
112
- const projectionRel = relative(projectionRoot, absPath);
113
- const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
114
- ? projectionRoot
115
- : gsdRoot(basePath);
116
- const rel = relative(root, absPath);
117
- // Normalize to forward slashes for consistent DB keys
118
- return rel.replace(/\\/g, "/");
112
+ return deriveCompatProjectionKey(absPath, [projectionRoot, gsdRoot(basePath)]);
119
113
  }
120
114
 
121
115
  /**
@@ -136,6 +136,14 @@ function cachedReaddir(dirPath: string): string[] {
136
136
  return entries;
137
137
  }
138
138
 
139
+ function isExistingFile(path: string): boolean {
140
+ try {
141
+ return statSync(path).isFile();
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
139
147
  /**
140
148
  * Clear the volatile directory listing caches.
141
149
  * Call after milestone transitions, file creation in planning directories,
@@ -284,7 +292,7 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
284
292
  if (!existsSync(dir)) return null;
285
293
  const target = `${idPrefix}-${suffix}.md`.toUpperCase();
286
294
  try {
287
- const entries = cachedReaddir(dir);
295
+ const entries = cachedReaddirWithTypes(dir).filter(e => e.isFile()).map(e => e.name);
288
296
  // Direct match: ID-SUFFIX.md
289
297
  const direct = entries.find(e => e.toUpperCase() === target);
290
298
  if (direct) return direct;
@@ -311,7 +319,9 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
311
319
  export function resolveTaskFiles(tasksDir: string, suffix: string): string[] {
312
320
  if (!existsSync(tasksDir)) return [];
313
321
  try {
314
- return cachedReaddir(tasksDir)
322
+ return cachedReaddirWithTypes(tasksDir)
323
+ .filter(e => e.isFile())
324
+ .map(e => e.name)
315
325
  .filter(f => taskIdFromTaskFileName(f, suffix) !== null)
316
326
  .sort();
317
327
  } catch {
@@ -860,7 +870,7 @@ export function resolveMilestoneFile(
860
870
  const prefix = `${String(phaseNum).padStart(2, "0")}`;
861
871
  const flatName = `${prefix}-${suffix}.md`;
862
872
  const flatPath = join(mDir, flatName);
863
- if (existsSync(flatPath)) return flatPath;
873
+ if (isExistingFile(flatPath)) return flatPath;
864
874
  // Legacy fallback: M001-SUFFIX.md
865
875
  const file = resolveFile(mDir, milestoneId, suffix);
866
876
  return file ? join(mDir, file) : null;
@@ -895,11 +905,11 @@ export function resolveSliceFile(
895
905
  const planNum = sliceIdToPlanNum(sliceId);
896
906
  const flatName = planFileName(phaseNum, planNum, suffix);
897
907
  const flatPath = join(phaseDir, flatName);
898
- if (existsSync(flatPath)) return flatPath;
908
+ if (isExistingFile(flatPath)) return flatPath;
899
909
  // Also check plan-number-only format MM-SUFFIX.md (written by buildSliceFileName)
900
910
  const planOnlyName = `${String(planNum).padStart(2, "0")}-${suffix}.md`;
901
911
  const planOnlyPath = join(phaseDir, planOnlyName);
902
- if (existsSync(planOnlyPath)) return planOnlyPath;
912
+ if (isExistingFile(planOnlyPath)) return planOnlyPath;
903
913
  // Try prefix match for the phase+plan number (handles suffix variations)
904
914
  const planPrefix = `${String(phaseNum).padStart(2, "0")}-${String(planNum).padStart(2, "0")}-`;
905
915
  try {
@@ -949,9 +959,9 @@ export function resolveTaskFile(
949
959
 
950
960
  if (suffix !== "PLAN" && !isLegacy) {
951
961
  const flatPath = join(phaseDir, buildFlatTaskFileName(sliceId, taskId, suffix));
952
- if (existsSync(flatPath)) return flatPath;
962
+ if (isExistingFile(flatPath)) return flatPath;
953
963
  const legacyFlatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
954
- return existsSync(legacyFlatPath) ? legacyFlatPath : null;
964
+ return isExistingFile(legacyFlatPath) ? legacyFlatPath : null;
955
965
  }
956
966
 
957
967
  const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
@@ -24,18 +24,18 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
24
24
 
25
25
  ### Step 1 - Dispatch Parallel Reviewers
26
26
 
27
- The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
27
+ The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. When a reviewer needs a full artifact, use only explicit file paths from the On-demand Validation Artifacts / On-demand Milestone Context blocks or discover files with `find .gsd -type f`; never pass a phase, slice, `tasks/`, or `slices/` directory to `read` or `readFileSync`. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
28
28
 
29
29
  Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
30
30
 
31
31
  **Reviewer A - Requirements Coverage**
32
- Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY under `.gsd/milestones/{{milestoneId}}/slices/` only if its preloaded excerpt is missing, truncated, or inconsistent. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
32
+ Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY only from an explicit file path in the On-demand Validation Artifacts list, or discover candidates with `find .gsd -type f -name '*-SUMMARY.md'`; never read a directory path. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
33
33
 
34
34
  **Reviewer B - Cross-Slice Integration**
35
- Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY only if the preloaded evidence is missing, truncated, or inconsistent. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
35
+ Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY from the On-demand Validation Artifacts list only if the preloaded evidence is missing, truncated, or inconsistent; never read a directory path. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
36
36
 
37
37
  **Reviewer C - Assessment & Acceptance Criteria**
38
- Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read `.gsd/milestones/{{milestoneId}}/{{milestoneId}}-CONTEXT.md`, a full SUMMARY, or a full ASSESSMENT only if the preloaded excerpt is missing, truncated, or inconsistent. UAT files are specs, not evidence. Verify each criterion maps to passing evidence. Then review the inlined `Verification Classes (from planning)` table. For every planned row in that table, output a `Verification Classes` table with columns `Class | Planned Check | Evidence | Verdict`. Preserve every planned non-empty class row; do not summarize, rename, combine, or omit planned classes. The first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT` when that class is present in planning. If a planned class lacks evidence, still include its canonical row and mark the verdict NEEDS-ATTENTION or FAIL. If a planned browser/UAT class has no ASSESSMENT with browser/runtime actions and assertions, return NEEDS-ATTENTION. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered by evidence, NEEDS-ATTENTION if gaps exist."
38
+ Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read the milestone context from the On-demand Milestone Context file path, and read full SUMMARY or ASSESSMENT files only from explicit paths in the On-demand Validation Artifacts list, if the preloaded excerpt is missing, truncated, or inconsistent. UAT files are specs, not evidence. Verify each criterion maps to passing evidence. Then review the inlined `Verification Classes (from planning)` table. For every planned row in that table, output a `Verification Classes` table with columns `Class | Planned Check | Evidence | Verdict`. Preserve every planned non-empty class row; do not summarize, rename, combine, or omit planned classes. The first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT` when that class is present in planning. If a planned class lacks evidence, still include its canonical row and mark the verdict NEEDS-ATTENTION or FAIL. If a planned browser/UAT class has no ASSESSMENT with browser/runtime actions and assertions, return NEEDS-ATTENTION. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered by evidence, NEEDS-ATTENTION if gaps exist."
39
39
 
40
40
  ### Step 2 - Synthesize Findings
41
41
 
@@ -85,6 +85,6 @@ If verdict is `needs-remediation`:
85
85
 
86
86
  **You MUST call `gsd_validate_milestone` before finishing. Do not manually write `{{validationPath}}`.**
87
87
 
88
- **File system safety:** When scanning milestone directories, use `ls` or `find` first. Never pass a directory path such as `tasks/` or `slices/` directly to `read`; it only accepts files.
88
+ **File system safety:** When scanning milestone directories, use `find .gsd -type f` first and read only files. In flat-phase projects, `.gsd/phases/<NN>-<slug>/` is a directory, not an artifact. Never pass a directory path such as a phase directory, `tasks/`, or `slices/` directly to `read`, `readFile`, or `readFileSync`.
89
89
 
90
90
  When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>." Say this exactly once — if you already said it in a prior message, do not repeat it.
@@ -9,13 +9,15 @@
9
9
  // refreshes the marker entry, closing the loop.
10
10
 
11
11
  import { existsSync, readFileSync } from "node:fs";
12
- import { join } from "node:path";
12
+ import { basename, join } from "node:path";
13
13
 
14
14
  import {
15
15
  computeProjectionSha,
16
16
  readCompatMarker,
17
17
  writeCompatMarker,
18
+ type ProjectionEntry,
18
19
  } from "../../compat/compat-marker.js";
20
+ import { _getAdapter } from "../../gsd-db.js";
19
21
  import { logWarning } from "../../workflow-logger.js";
20
22
  import type { GSDState } from "../../types.js";
21
23
  import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
@@ -25,6 +27,102 @@ type ExternalMarkdownEditDrift = Extract<
25
27
  { kind: "external-markdown-edit" }
26
28
  >;
27
29
 
30
+ type ArtifactContentRow = {
31
+ full_content: string;
32
+ };
33
+
34
+ type ProjectionScope = {
35
+ milestoneId: string;
36
+ sliceId: string | null;
37
+ taskId: string | null;
38
+ };
39
+
40
+ function deepestProjectionScope(entities: readonly string[]): ProjectionScope | null {
41
+ let best: string[] | null = null;
42
+ for (const entity of entities) {
43
+ const parts = entity.split("/").filter(Boolean);
44
+ if (parts.length === 0) continue;
45
+ if (!best || parts.length > best.length) best = parts;
46
+ }
47
+ if (!best) return null;
48
+ return {
49
+ milestoneId: best[0]!,
50
+ sliceId: best[1] ?? null,
51
+ taskId: best[2] ?? null,
52
+ };
53
+ }
54
+
55
+ function artifactTypeFromProjectionPath(projectionPath: string): string | null {
56
+ const name = basename(projectionPath);
57
+ const match = name.match(/^(?:[A-Za-z]\d+|\d+(?:-\d+)?)-(.+)\.md$/);
58
+ return match?.[1]?.toUpperCase() ?? null;
59
+ }
60
+
61
+ function dbProjectionMatches(
62
+ projectionPath: string,
63
+ entry: ProjectionEntry,
64
+ actualSha: string,
65
+ ): boolean {
66
+ const adapter = _getAdapter();
67
+ if (!adapter) return false;
68
+
69
+ try {
70
+ const rows: ArtifactContentRow[] = adapter
71
+ .prepare("SELECT full_content FROM artifacts WHERE path = :path")
72
+ .all({ ":path": projectionPath }) as ArtifactContentRow[];
73
+
74
+ const scope = deepestProjectionScope(entry.entities);
75
+ const artifactType = artifactTypeFromProjectionPath(projectionPath);
76
+ if (scope?.taskId) {
77
+ rows.push(
78
+ ...(adapter
79
+ .prepare(
80
+ `SELECT full_content
81
+ FROM artifacts
82
+ WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid
83
+ AND (:type IS NULL OR artifact_type = :type)`,
84
+ )
85
+ .all({
86
+ ":mid": scope.milestoneId,
87
+ ":sid": scope.sliceId,
88
+ ":tid": scope.taskId,
89
+ ":type": artifactType,
90
+ }) as ArtifactContentRow[]),
91
+ );
92
+ } else if (scope?.sliceId) {
93
+ rows.push(
94
+ ...(adapter
95
+ .prepare(
96
+ `SELECT full_content
97
+ FROM artifacts
98
+ WHERE milestone_id = :mid AND slice_id = :sid AND task_id IS NULL
99
+ AND (:type IS NULL OR artifact_type = :type)`,
100
+ )
101
+ .all({
102
+ ":mid": scope.milestoneId,
103
+ ":sid": scope.sliceId,
104
+ ":type": artifactType,
105
+ }) as ArtifactContentRow[]),
106
+ );
107
+ } else if (scope) {
108
+ rows.push(
109
+ ...(adapter
110
+ .prepare(
111
+ `SELECT full_content
112
+ FROM artifacts
113
+ WHERE milestone_id = :mid AND slice_id IS NULL AND task_id IS NULL
114
+ AND (:type IS NULL OR artifact_type = :type)`,
115
+ )
116
+ .all({ ":mid": scope.milestoneId, ":type": artifactType }) as ArtifactContentRow[]),
117
+ );
118
+ }
119
+
120
+ return rows.some((row) => computeProjectionSha(row.full_content) === actualSha);
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
28
126
  /**
29
127
  * Detect sha drift between the marker baseline and current file contents.
30
128
  *
@@ -38,16 +136,43 @@ function detectExternalMarkdownEdit(
38
136
  _state: GSDState,
39
137
  ctx: DriftContext,
40
138
  ): ExternalMarkdownEditDrift[] {
41
- const marker = readCompatMarker(ctx.basePath);
139
+ const marker = readCompatMarker(ctx.basePath, {
140
+ healInvalidKeys: !ctx.dryRun,
141
+ quarantineInvalid: !ctx.dryRun,
142
+ });
42
143
  const entries = Object.entries(marker.projections);
43
144
  if (entries.length === 0) return [];
44
145
 
45
146
  const records: ExternalMarkdownEditDrift[] = [];
147
+ let markerChanged = false;
46
148
  for (const [projectionPath, entry] of entries) {
47
149
  const abs = join(ctx.basePath, ".gsd", projectionPath);
48
150
  if (!existsSync(abs)) continue;
49
151
  const actual = computeProjectionSha(readFileSync(abs, "utf-8"));
50
152
  if (actual === entry.sha) continue;
153
+ if (dbProjectionMatches(projectionPath, entry, actual)) {
154
+ if (!ctx.dryRun) {
155
+ marker.projections[projectionPath] = {
156
+ sha: actual,
157
+ entities: entry.entities,
158
+ };
159
+ markerChanged = true;
160
+ logWarning(
161
+ "reconcile",
162
+ `external-markdown-edit: refreshed stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
163
+ );
164
+ } else {
165
+ logWarning(
166
+ "reconcile",
167
+ `external-markdown-edit: stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
168
+ );
169
+ }
170
+ continue;
171
+ }
172
+ logWarning(
173
+ "reconcile",
174
+ `external-markdown-edit drift for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
175
+ );
51
176
  records.push({
52
177
  kind: "external-markdown-edit",
53
178
  projectionPath,
@@ -56,6 +181,11 @@ function detectExternalMarkdownEdit(
56
181
  entities: entry.entities,
57
182
  });
58
183
  }
184
+ if (markerChanged) {
185
+ marker.lastWriter = "gsd-pi";
186
+ marker.lastProjectedAt = new Date().toISOString();
187
+ writeCompatMarker(ctx.basePath, marker);
188
+ }
59
189
  return records;
60
190
  }
61
191
 
@@ -82,7 +82,10 @@ async function detectExternalPlanningEdit(
82
82
  _state: GSDState,
83
83
  ctx: DriftContext,
84
84
  ): Promise<ExternalPlanningEditDrift[]> {
85
- const marker = readCompatMarker(ctx.basePath);
85
+ const marker = readCompatMarker(ctx.basePath, {
86
+ healInvalidKeys: !ctx.dryRun,
87
+ quarantineInvalid: !ctx.dryRun,
88
+ });
86
89
  if (!marker.planning?.active) {
87
90
  // Not yet activated. Activation (layout parse + DB import) is owned by
88
91
  // capturePlanningCompatIfNeeded, called from reconcileBeforeDispatch when
@@ -16,7 +16,7 @@ import { renderRoadmapFromDb } from "../../markdown-renderer.js";
16
16
  import { findMilestoneIds } from "../../milestone-ids.js";
17
17
  import { parseRoadmap } from "../../parsers-legacy.js";
18
18
  import { resolveMilestoneFile } from "../../paths.js";
19
- import { isClosedStatus } from "../../status-guards.js";
19
+ import { isClosedStatus, isSkippedForDispatch } from "../../status-guards.js";
20
20
  import type { GSDState } from "../../types.js";
21
21
  import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
22
22
 
@@ -94,7 +94,9 @@ export function detectRoadmapDivergenceDrift(
94
94
  for (const milestoneId of findMilestoneIds(ctx.basePath)) {
95
95
  // Skip milestones that don't yet have a DB row — that's the
96
96
  // unregistered-milestone drift handler's responsibility.
97
- if (!getMilestone(milestoneId)) continue;
97
+ const milestone = getMilestone(milestoneId);
98
+ if (!milestone) continue;
99
+ if (isSkippedForDispatch(milestone.status)) continue;
98
100
  if (milestoneHasDivergence(ctx.basePath, milestoneId)) {
99
101
  drifts.push({ kind: "roadmap-divergence", milestoneId });
100
102
  }
@@ -29,6 +29,14 @@ export function formatStopNoticePrefix(reason?: string | null): string {
29
29
  return displayReason ? `${prefix} — ${displayReason}` : prefix;
30
30
  }
31
31
 
32
+ export function formatVerdictRecordedNotice(message: string): string {
33
+ return `Verdict recorded: ${message}`;
34
+ }
35
+
36
+ export function formatVerdictRejectedNotice(message: string): string {
37
+ return `Verdict rejected: ${message}`;
38
+ }
39
+
32
40
  // ─── Classification (headless host side) ────────────────────────────────
33
41
  // The canonical lowercase prefixes the headless event loop recognizes in
34
42
  // notify messages. Emitters above and ad-hoc emitters elsewhere must start
@@ -42,6 +50,8 @@ export const TERMINAL_NOTICE_PREFIXES = [
42
50
  "auto-mode complete",
43
51
  "no active milestone",
44
52
  "auto-mode idle",
53
+ "verdict recorded",
54
+ "verdict rejected",
45
55
  ] as const;
46
56
 
47
57
  /** Manual-resolution notices emitted before auto-mode can formally pause/stop. */
@@ -85,6 +95,7 @@ export function isInteractiveMenuUnavailableNotice(message: string): boolean {
85
95
  export function isBlockedNoticeMessage(message: string): boolean {
86
96
  return (
87
97
  message.includes("blocked:") ||
98
+ message.startsWith("verdict rejected") ||
88
99
  (isPauseNotice(message) && !isNonBlockingPauseNotice(message)) ||
89
100
  isManualResolutionNotice(message) ||
90
101
  isInteractiveMenuUnavailableNotice(message)