@opengsd/gsd-pi 1.7.0 → 1.8.1

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/README.md +8 -6
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/doctor-history.js +65 -0
  4. package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
  5. package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
  6. package/dist/resources/extensions/gsd/doctor.js +14 -653
  7. package/dist/resources/extensions/gsd/md-importer.js +7 -2
  8. package/dist/resources/extensions/gsd/migrate/planning-writer.js +8 -2
  9. package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
  10. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
  11. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
  12. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  13. package/dist/web/standalone/.next/BUILD_ID +1 -1
  14. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  15. package/dist/web/standalone/.next/build-manifest.json +2 -2
  16. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.html +1 -1
  33. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  44. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  45. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  46. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  47. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  48. package/integrations/hermes/pyproject.toml +1 -1
  49. package/package.json +6 -6
  50. package/packages/cloud-mcp-gateway/package.json +2 -2
  51. package/packages/contracts/package.json +1 -1
  52. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  53. package/packages/daemon/dist/cloud-config.js +25 -7
  54. package/packages/daemon/dist/cloud-config.js.map +1 -1
  55. package/packages/daemon/dist/cloud-config.test.js +19 -1
  56. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  57. package/packages/daemon/package.json +4 -4
  58. package/packages/gsd-agent-core/package.json +5 -5
  59. package/packages/gsd-agent-modes/package.json +7 -7
  60. package/packages/gsd-cloud/README.md +18 -4
  61. package/packages/gsd-cloud/bin/gsd-cloud.js +6 -5
  62. package/packages/gsd-cloud/package.json +6 -4
  63. package/packages/mcp-server/package.json +4 -4
  64. package/packages/native/package.json +1 -1
  65. package/packages/pi-agent-core/package.json +1 -1
  66. package/packages/pi-ai/package.json +1 -1
  67. package/packages/pi-coding-agent/package.json +7 -7
  68. package/packages/pi-tui/package.json +2 -2
  69. package/packages/rpc-client/package.json +2 -2
  70. package/pkg/package.json +1 -1
  71. package/src/resources/extensions/gsd/doctor-history.ts +81 -0
  72. package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
  73. package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
  74. package/src/resources/extensions/gsd/doctor.ts +14 -673
  75. package/src/resources/extensions/gsd/md-importer.ts +7 -2
  76. package/src/resources/extensions/gsd/migrate/planning-writer.ts +9 -3
  77. package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
  78. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
  79. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
  80. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
  81. package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
  82. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
  83. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
  84. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
  85. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +18 -1
  86. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
  87. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
  88. /package/dist/web/standalone/.next/static/{enWQFRY-aLTUq3wmxG2Qz → UaXRrOYxBpG9QBINKhZkg}/_buildManifest.js +0 -0
  89. /package/dist/web/standalone/.next/static/{enWQFRY-aLTUq3wmxG2Qz → UaXRrOYxBpG9QBINKhZkg}/_ssgManifest.js +0 -0
@@ -107,11 +107,11 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
107
107
  sketchScope: "limited",
108
108
  });
109
109
 
110
- // Simulate the post-crash scenario: PLAN.md exists on disk but the
111
- // is_sketch flag is still 1.
110
+ // Simulate the post-crash scenario: a *real* PLAN.md (a decomposed task)
111
+ // exists on disk but the is_sketch flag is still 1.
112
112
  writeFileSync(
113
113
  join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
114
- "# S02 Plan\n",
114
+ makeStalePlanContent("S02", [{ id: "T01", title: "Build the feature", done: false }]),
115
115
  );
116
116
  assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "pre: flagged as sketch");
117
117
 
@@ -131,6 +131,120 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
131
131
  }
132
132
  });
133
133
 
134
+ test("#1287: stub/placeholder PLAN does NOT clear the sketch flag", async (t) => {
135
+ const base = makeFixtureBase();
136
+ t.after(() => cleanup(base));
137
+
138
+ openDatabase(join(base, ".gsd", "gsd.db"));
139
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
140
+ insertSlice({
141
+ id: "S02",
142
+ milestoneId: "M001",
143
+ title: "Feature",
144
+ status: "pending",
145
+ risk: "medium",
146
+ depends: [],
147
+ demo: "S02 demo.",
148
+ sequence: 1,
149
+ isSketch: true,
150
+ sketchScope: "limited",
151
+ });
152
+
153
+ const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
154
+
155
+ // A bare stub PLAN (no decomposed tasks) must not clear the flag.
156
+ writeFileSync(planPath, "# S02 Plan\n");
157
+ clearRendererCaches();
158
+ let state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
159
+ let result = await reconcileBeforeDispatch(base, {
160
+ invalidateStateCache: () => {},
161
+ deriveState: async () => state,
162
+ });
163
+ assert.equal(result.ok, true);
164
+ assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "stub PLAN: flag stays set");
165
+ assert.equal(result.repaired.length, 0, "stub PLAN: no repair");
166
+
167
+ // A projection round-trip stub whose only task is a synthetic "Plan NN"
168
+ // placeholder (migrate/transformer.buildTaskTitle) must also not clear it.
169
+ writeFileSync(
170
+ planPath,
171
+ makeStalePlanContent("S02", [{ id: "T01", title: "Plan 01", done: false }]),
172
+ );
173
+ clearRendererCaches();
174
+ state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
175
+ result = await reconcileBeforeDispatch(base, {
176
+ invalidateStateCache: () => {},
177
+ deriveState: async () => state,
178
+ });
179
+ assert.equal(result.ok, true);
180
+ assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "placeholder task: flag stays set");
181
+ assert.equal(result.repaired.length, 0, "placeholder task: no repair");
182
+
183
+ // buildTaskTitle also emits `${phase} ${plan}` (e.g. "00 01") when the plan
184
+ // frontmatter carries phase/plan. This projected placeholder must not clear
185
+ // the flag either.
186
+ writeFileSync(
187
+ planPath,
188
+ makeStalePlanContent("S02", [{ id: "T01", title: "00 01", done: false }]),
189
+ );
190
+ clearRendererCaches();
191
+ state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
192
+ result = await reconcileBeforeDispatch(base, {
193
+ invalidateStateCache: () => {},
194
+ deriveState: async () => state,
195
+ });
196
+ assert.equal(result.ok, true);
197
+ assert.equal(
198
+ getSlice("M001", "S02")?.is_sketch,
199
+ 1,
200
+ "phase/plan placeholder task: flag stays set",
201
+ );
202
+ assert.equal(result.repaired.length, 0, "phase/plan placeholder task: no repair");
203
+ });
204
+
205
+ test("#1288: real tasks shaped like `word + number` still clear the sketch flag", async (t) => {
206
+ const base = makeFixtureBase();
207
+ t.after(() => cleanup(base));
208
+
209
+ openDatabase(join(base, ".gsd", "gsd.db"));
210
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
211
+ insertSlice({
212
+ id: "S02",
213
+ milestoneId: "M001",
214
+ title: "Feature",
215
+ status: "pending",
216
+ risk: "medium",
217
+ depends: [],
218
+ demo: "S02 demo.",
219
+ sequence: 1,
220
+ isSketch: true,
221
+ sketchScope: "limited",
222
+ });
223
+
224
+ const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
225
+
226
+ // `Step 1` / `RFC 1234` match the loose `word + number` shape but are genuine
227
+ // decomposed tasks. buildTaskTitle only emits `${phase} ${plan}` with a
228
+ // digit-led phase, so these must NOT be read as placeholders (#1288): a real
229
+ // plan-slice like this must still clear the stale is_sketch flag.
230
+ writeFileSync(
231
+ planPath,
232
+ makeStalePlanContent("S02", [
233
+ { id: "T01", title: "Step 1", done: false },
234
+ { id: "T02", title: "RFC 1234", done: false },
235
+ ]),
236
+ );
237
+ clearRendererCaches();
238
+ const state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
239
+ const result = await reconcileBeforeDispatch(base, {
240
+ invalidateStateCache: () => {},
241
+ deriveState: async () => state,
242
+ });
243
+ assert.equal(result.ok, true);
244
+ assert.equal(getSlice("M001", "S02")?.is_sketch, 0, "real task titles: flag cleared");
245
+ assert.equal(result.repaired.length, 1, "real task titles: repaired once");
246
+ });
247
+
134
248
  test("ADR-017 (#5700): repair failure throws ReconciliationFailedError with shape", async () => {
135
249
  const seenDrift: DriftRecord = { kind: "stale-sketch-flag", mid: "M001", sid: "S02" };
136
250
  const handler: DriftHandler = {
@@ -1032,7 +1146,7 @@ test("ADR-017 (#5703): live worker lock is not cleared", async (t) => {
1032
1146
 
1033
1147
  // ─── #5704: unregistered-milestone drift ────────────────────────────────────
1034
1148
 
1035
- test("ADR-017 (#5704): unregistered-milestone drift fails closed without importing markdown", async (t) => {
1149
+ test("ADR-017 (#5704/#1281): unregistered-milestone drift pauses with a hint instead of hard-escalating", async (t) => {
1036
1150
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-"));
1037
1151
  const milestoneDir = join(base, ".gsd", "phases", "42-test");
1038
1152
  mkdirSync(milestoneDir, { recursive: true });
@@ -1059,29 +1173,67 @@ test("ADR-017 (#5704): unregistered-milestone drift fails closed without importi
1059
1173
  // Pre-condition: filesystem has the milestone, DB does NOT.
1060
1174
  assert.equal(getMilestone("M042"), null, "pre: DB has no row for M042");
1061
1175
 
1062
- await assert.rejects(
1063
- reconcileBeforeDispatch(base, {
1064
- invalidateStateCache: () => {},
1065
- deriveState: async () => makeState(),
1066
- }),
1067
- (err: unknown) => {
1068
- assert.ok(err instanceof ReconciliationFailedError);
1069
- assert.match(String(err.message), /unregistered-milestone/);
1070
- assert.equal(err.failures[0]?.drift.kind, "unregistered-milestone");
1071
- assert.match(String(err.failures[0]?.cause), /M042/);
1072
- assert.match(String(err.failures[0]?.cause), /markdown projection/);
1073
- // Hint leads with targeted, non-destructive actions (rename/discard)...
1074
- assert.match(String(err.failures[0]?.cause), /Rename/);
1075
- assert.match(String(err.failures[0]?.cause), /Discard/);
1076
- // ...and reframes recover as a destructive last resort, not the fix (#826).
1077
- assert.match(String(err.failures[0]?.cause), /\/gsd recover/);
1078
- assert.match(String(err.failures[0]?.cause), /last resort/i);
1079
- return true;
1080
- },
1081
- );
1176
+ // #1281: the handler exposes a `blocker`, so reconciliation returns a
1177
+ // non-fatal pause-with-hint (ok:true + blocker) rather than throwing.
1178
+ const result = await reconcileBeforeDispatch(base, {
1179
+ invalidateStateCache: () => {},
1180
+ deriveState: async () => makeState(),
1181
+ });
1182
+ assert.equal(result.ok, true);
1183
+ const blocker = result.blockers.find((b) => /M042/.test(b));
1184
+ assert.ok(blocker, "expected an M042 unregistered-milestone blocker");
1185
+ assert.match(blocker!, /markdown projection/);
1186
+ // Hint leads with targeted, non-destructive actions (rename/discard)...
1187
+ assert.match(blocker!, /Rename/);
1188
+ assert.match(blocker!, /Discard/);
1189
+ // ...and reframes recover as a destructive last resort, not the fix (#826).
1190
+ assert.match(blocker!, /\/gsd recover/);
1191
+ assert.match(blocker!, /last resort/i);
1192
+ // Runtime never imports markdown into the DB.
1082
1193
  assert.equal(getMilestone("M042"), null, "post: DB still has no row for M042");
1083
1194
  });
1084
1195
 
1196
+ test("#1281: descriptive flat-phase dir registered under a suffixed id → no false-positive drift", async (t) => {
1197
+ const base = mkdtempSync(join(tmpdir(), "gsd-1281-suffix-"));
1198
+ // Descriptive slug the flat-phase extractor cannot recover the suffix from →
1199
+ // it derives the bare `M007`, which has no DB row.
1200
+ const milestoneDir = join(base, ".gsd", "phases", "07-v40fmq-m007-v40fmq-navigation-footer-system");
1201
+ mkdirSync(milestoneDir, { recursive: true });
1202
+ writeFileSync(
1203
+ join(milestoneDir, "M007-v40fmq-ROADMAP.md"),
1204
+ [
1205
+ "# M007: Navigation Footer System",
1206
+ "",
1207
+ "**Vision:** Verify no false-positive unregistered-milestone drift",
1208
+ "",
1209
+ "## Slices",
1210
+ "",
1211
+ "- [ ] **S01: Foundation** `risk:medium` `depends:[]`",
1212
+ "",
1213
+ ].join("\n"),
1214
+ );
1215
+ t.after(() => {
1216
+ try { closeDatabase(); } catch { /* noop */ }
1217
+ rmSync(base, { recursive: true, force: true });
1218
+ });
1219
+
1220
+ openDatabase(join(base, ".gsd", "gsd.db"));
1221
+ // The milestone IS registered — under the unique_milestone_ids suffixed id.
1222
+ insertMilestone({ id: "M007-v40fmq", title: "Navigation Footer System", status: "complete" });
1223
+
1224
+ const result = await reconcileBeforeDispatch(base, {
1225
+ invalidateStateCache: () => {},
1226
+ deriveState: async () => makeState(),
1227
+ });
1228
+
1229
+ assert.equal(result.ok, true);
1230
+ assert.equal(
1231
+ result.blockers.some((b) => /unregistered/i.test(b) || /M007/.test(b)),
1232
+ false,
1233
+ "bare M007 derived from the slug must resolve to the registered M007-v40fmq row",
1234
+ );
1235
+ });
1236
+
1085
1237
  test("ADR-017 (#5704): registered milestone (DB row present) → no drift", async (t) => {
1086
1238
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-clean-"));
1087
1239
  const milestoneDir = join(base, ".gsd", "phases", "01-test");
@@ -1977,7 +2129,7 @@ test("deriveState is pure: stale sketch healed only via reconcileBeforeDispatch"
1977
2129
  });
1978
2130
  writeFileSync(
1979
2131
  join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
1980
- "# S02 Plan\n",
2132
+ makeStalePlanContent("S02", [{ id: "T01", title: "Build the feature", done: false }]),
1981
2133
  );
1982
2134
 
1983
2135
  const { deriveState } = await import("../state.ts");