@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.
- package/README.md +1 -1
- package/dist/cli.js +17 -4
- package/dist/headless-answers.js +1 -1
- package/dist/headless-ui.d.ts +1 -1
- package/dist/headless-ui.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
- package/dist/resources/extensions/gsd/auto-budget.js +5 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +5 -2
- package/dist/resources/extensions/gsd/auto.js +19 -10
- package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-usage.js +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +4 -1
- package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
- package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
- package/dist/resources/extensions/gsd/git-service.js +57 -0
- package/dist/resources/extensions/gsd/gitignore.js +4 -1
- package/dist/resources/extensions/gsd/json-persistence.js +20 -0
- package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
- package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
- package/dist/resources/extensions/gsd/metrics.js +4 -4
- package/dist/resources/extensions/gsd/migrate-external.js +11 -2
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
- package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
- package/dist/resources/extensions/gsd/undo.js +43 -6
- package/dist/resources/extensions/gsd/unit-registry.js +4 -2
- package/dist/resources/extensions/gsd/verification-gate.js +7 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +3 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +46 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +38 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/session.ts +9 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
- package/src/resources/extensions/gsd/auto-budget.ts +12 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-verification.ts +5 -1
- package/src/resources/extensions/gsd/auto.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-usage.ts +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +8 -1
- package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
- package/src/resources/extensions/gsd/doctor-types.ts +3 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
- package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
- package/src/resources/extensions/gsd/git-service.ts +64 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/json-persistence.ts +20 -0
- package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
- package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
- package/src/resources/extensions/gsd/metrics.ts +4 -4
- package/src/resources/extensions/gsd/migrate-external.ts +8 -2
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
- package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
- package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
- package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
- package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
- package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
- package/src/resources/extensions/gsd/undo.ts +47 -6
- package/src/resources/extensions/gsd/unit-registry.ts +4 -2
- package/src/resources/extensions/gsd/verification-gate.ts +8 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import type { Dirent } from "node:fs";
|
|
15
16
|
import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
16
17
|
import { gsdRoot } from "./paths.js";
|
|
17
18
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
@@ -360,6 +361,7 @@ export interface PreMergeCheckResult {
|
|
|
360
361
|
*/
|
|
361
362
|
export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
|
|
362
363
|
".gsd-worktrees/",
|
|
364
|
+
".gsd-backups/**",
|
|
363
365
|
".gsd/activity/",
|
|
364
366
|
".gsd/audit/",
|
|
365
367
|
".gsd/forensics/",
|
|
@@ -1268,6 +1270,7 @@ function collectRepositoryDirtyStatus(basePath: string): Record<string, boolean>
|
|
|
1268
1270
|
const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
|
|
1269
1271
|
const registry = createRepositoryRegistryFromPreferences(basePath, preferences);
|
|
1270
1272
|
const dirtyByRepository: Record<string, boolean> = {};
|
|
1273
|
+
const registeredRoots = new Set(registry.repositories.map((repo) => resolve(repo.root)));
|
|
1271
1274
|
for (const repo of registry.repositories) {
|
|
1272
1275
|
try {
|
|
1273
1276
|
dirtyByRepository[repo.id] = runGit(repo.root, ["status", "--porcelain"]).length > 0;
|
|
@@ -1276,9 +1279,70 @@ function collectRepositoryDirtyStatus(basePath: string): Record<string, boolean>
|
|
|
1276
1279
|
dirtyByRepository[repo.id] = nativeHasChanges(repo.root);
|
|
1277
1280
|
}
|
|
1278
1281
|
}
|
|
1282
|
+
for (const repoRoot of findUndeclaredNestedGitRepositories(registry.projectRoot, registeredRoots)) {
|
|
1283
|
+
let dirty = false;
|
|
1284
|
+
try {
|
|
1285
|
+
dirty = runGit(repoRoot, ["status", "--porcelain"]).length > 0;
|
|
1286
|
+
} catch {
|
|
1287
|
+
dirty = nativeHasChanges(repoRoot);
|
|
1288
|
+
}
|
|
1289
|
+
if (!dirty) continue;
|
|
1290
|
+
|
|
1291
|
+
const relPath = relative(registry.projectRoot, repoRoot).split(sep).join("/") || repoRoot;
|
|
1292
|
+
const label = `${relPath} (undeclared)`;
|
|
1293
|
+
dirtyByRepository[label] = true;
|
|
1294
|
+
logWarning(
|
|
1295
|
+
"engine",
|
|
1296
|
+
`undeclared dirty nested git repo ${relPath}; declare it under workspace.repositories or commit manually`,
|
|
1297
|
+
{ file: "git-service.ts" },
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1279
1300
|
return dirtyByRepository;
|
|
1280
1301
|
}
|
|
1281
1302
|
|
|
1303
|
+
const UNDECLARED_NESTED_REPO_SCAN_MAX_DEPTH = 4;
|
|
1304
|
+
const UNDECLARED_NESTED_REPO_SKIP_DIRS = new Set([
|
|
1305
|
+
".git",
|
|
1306
|
+
".gsd",
|
|
1307
|
+
".gsd-worktrees",
|
|
1308
|
+
"node_modules",
|
|
1309
|
+
"dist",
|
|
1310
|
+
"dist-test",
|
|
1311
|
+
]);
|
|
1312
|
+
|
|
1313
|
+
function findUndeclaredNestedGitRepositories(projectRoot: string, registeredRoots: ReadonlySet<string>): string[] {
|
|
1314
|
+
const found: string[] = [];
|
|
1315
|
+
|
|
1316
|
+
function visit(dir: string, depth: number): void {
|
|
1317
|
+
if (depth >= UNDECLARED_NESTED_REPO_SCAN_MAX_DEPTH) return;
|
|
1318
|
+
|
|
1319
|
+
let entries: Dirent[];
|
|
1320
|
+
try {
|
|
1321
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1322
|
+
} catch {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
for (const entry of entries) {
|
|
1327
|
+
if (!entry.isDirectory()) continue;
|
|
1328
|
+
if (UNDECLARED_NESTED_REPO_SKIP_DIRS.has(entry.name)) continue;
|
|
1329
|
+
|
|
1330
|
+
const child = join(dir, entry.name);
|
|
1331
|
+
const childRoot = resolve(child);
|
|
1332
|
+
const hasGitDir = existsSync(join(child, ".git"));
|
|
1333
|
+
const isRegistered = registeredRoots.has(childRoot);
|
|
1334
|
+
if (hasGitDir && !isRegistered) {
|
|
1335
|
+
found.push(childRoot);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
visit(child, depth + 1);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
visit(projectRoot, 0);
|
|
1343
|
+
return found;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1282
1346
|
function runPerRepositoryCommitAction(args: {
|
|
1283
1347
|
basePath: string;
|
|
1284
1348
|
unitType: string;
|
|
@@ -24,8 +24,9 @@ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
|
24
24
|
* With external state (symlink), these are a no-op in most cases,
|
|
25
25
|
* but retained for backwards compatibility during migration.
|
|
26
26
|
*/
|
|
27
|
-
const GSD_RUNTIME_PATTERNS = [
|
|
27
|
+
export const GSD_RUNTIME_PATTERNS = [
|
|
28
28
|
".gsd-worktrees/",
|
|
29
|
+
".gsd-backups/",
|
|
29
30
|
".gsd/activity/",
|
|
30
31
|
".gsd/audit/",
|
|
31
32
|
".gsd/forensics/",
|
|
@@ -51,6 +52,8 @@ const BASELINE_PATTERNS = [
|
|
|
51
52
|
".gsd",
|
|
52
53
|
// Worktree container sibling — NOT covered by the ".gsd" pattern above.
|
|
53
54
|
".gsd-worktrees/",
|
|
55
|
+
// Flat-phase migration snapshots — NOT covered by the ".gsd" pattern above.
|
|
56
|
+
".gsd-backups/",
|
|
54
57
|
".gsd-id",
|
|
55
58
|
".mcp.json",
|
|
56
59
|
".bg-shell/",
|
|
@@ -62,6 +62,26 @@ export function saveJsonFile<T>(filePath: string, data: T): void {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Save a JSON file atomically using compact serialization.
|
|
67
|
+
*
|
|
68
|
+
* Intended for machine-only ledgers where pretty-printing has no consumer.
|
|
69
|
+
* Keeps the same write-tmp-rename safety and non-fatal error behavior as
|
|
70
|
+
* saveJsonFile.
|
|
71
|
+
*/
|
|
72
|
+
export function saveJsonFileCompact<T>(filePath: string, data: T): void {
|
|
73
|
+
try {
|
|
74
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
75
|
+
// Use randomized tmp suffix to prevent concurrent-write data loss
|
|
76
|
+
const tmp = `${filePath}.tmp.${randomBytes(4).toString("hex")}`;
|
|
77
|
+
writeFileSync(tmp, JSON.stringify(data) + "\n", "utf-8");
|
|
78
|
+
renameSync(tmp, filePath);
|
|
79
|
+
// No cleanup needed — renameSync atomically removes tmp on success
|
|
80
|
+
} catch {
|
|
81
|
+
// Non-fatal — don't let persistence failures break operation
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
/**
|
|
66
86
|
* Write a JSON file atomically (write to .tmp, then rename).
|
|
67
87
|
* Creates parent directories as needed. Non-fatal on error.
|
|
@@ -35,8 +35,8 @@ interface SynthesizedRow {
|
|
|
35
35
|
* - Idempotent (per-row): each migrated memory carries
|
|
36
36
|
* `structured_fields.sourceKnowledgeId = "<P|L>NNN"`. Rows whose ID is
|
|
37
37
|
* already present in the memory store are skipped.
|
|
38
|
-
* - Best-effort: never throws.
|
|
39
|
-
* backfill cannot block
|
|
38
|
+
* - Best-effort: never throws. Failed memory inserts are logged and skipped;
|
|
39
|
+
* setup/read failures log and return 0 so backfill cannot block startup.
|
|
40
40
|
* - Rules (K###) are intentionally skipped — they remain manually maintained
|
|
41
41
|
* in `KNOWLEDGE.md` per ADR-013.
|
|
42
42
|
*
|
|
@@ -69,14 +69,20 @@ export function backfillKnowledgeToMemories(basePath: string): number {
|
|
|
69
69
|
const matchPattern = `%"sourceKnowledgeId":"${synth.id}"%`;
|
|
70
70
|
if (checkExisting.get({ ":pattern": matchPattern })) continue;
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
try {
|
|
73
|
+
const id = createMemory({
|
|
74
|
+
category: synth.category,
|
|
75
|
+
content: synth.content,
|
|
76
|
+
scope: synth.scope,
|
|
77
|
+
confidence: 0.85,
|
|
78
|
+
structuredFields: synth.structuredFields,
|
|
79
|
+
});
|
|
80
|
+
if (id) written += 1;
|
|
81
|
+
} catch (rowErr) {
|
|
82
|
+
const message = rowErr instanceof Error ? rowErr.message : String(rowErr);
|
|
83
|
+
logWarning("knowledge-backfill", `KNOWLEDGE.md -> memories backfill skipped ${synth.id}: ${message}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
return written;
|
|
@@ -49,8 +49,8 @@ type DecisionContentFields = Pick<DecisionRow, "decision" | "choice" | "rational
|
|
|
49
49
|
* decision is checked individually; only decisions whose id is already
|
|
50
50
|
* present in the memory store are skipped. A user-authored memory with
|
|
51
51
|
* their own `sourceDecisionId` does NOT abort the backfill.
|
|
52
|
-
* - Best-effort: never throws.
|
|
53
|
-
* backfill cannot block
|
|
52
|
+
* - Best-effort: never throws. Failed memory inserts are logged and skipped;
|
|
53
|
+
* setup/read failures log and return 0 so backfill cannot block startup.
|
|
54
54
|
* - Full migration (ADR-013 Stage 2a): both active and superseded rows are
|
|
55
55
|
* migrated. `structured_fields.superseded_by` preserves the supersedes
|
|
56
56
|
* chain so the DECISIONS.md projection regen can source from memories
|
|
@@ -146,24 +146,30 @@ export function backfillDecisionsToMemories(): number {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const content = synthesizeDecisionMemoryContent(row);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
try {
|
|
150
|
+
const id = createMemory({
|
|
151
|
+
category: "architecture",
|
|
152
|
+
content,
|
|
153
|
+
scope: row.scope || "project",
|
|
154
|
+
confidence: 0.85,
|
|
155
|
+
structuredFields: {
|
|
156
|
+
sourceDecisionId: row.id,
|
|
157
|
+
when_context: row.when_context,
|
|
158
|
+
scope: row.scope,
|
|
159
|
+
decision: row.decision,
|
|
160
|
+
choice: row.choice,
|
|
161
|
+
rationale: row.rationale,
|
|
162
|
+
made_by: row.made_by,
|
|
163
|
+
revisable: row.revisable,
|
|
164
|
+
superseded_by: row.superseded_by,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (id) written += 1;
|
|
168
|
+
} catch (rowErr) {
|
|
169
|
+
const message = rowErr instanceof Error ? rowErr.message : String(rowErr);
|
|
170
|
+
logWarning("memory-backfill", `decisions->memories backfill skipped ${row.id}: ${message}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
if (healed > 0) {
|
|
@@ -19,7 +19,7 @@ import { openSync, closeSync, unlinkSync, statSync, writeFileSync } from "node:f
|
|
|
19
19
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
20
20
|
import { gsdRoot } from "./paths.js";
|
|
21
21
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
22
|
-
import { loadJsonFile, loadJsonFileOrNull,
|
|
22
|
+
import { loadJsonFile, loadJsonFileOrNull, saveJsonFileCompact } from "./json-persistence.js";
|
|
23
23
|
import { parseUnitId } from "./unit-id.js";
|
|
24
24
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
25
25
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
@@ -873,7 +873,7 @@ export function pruneMetricsLedger(base: string, keepCount: number): number {
|
|
|
873
873
|
if (!disk || disk.units.length <= keepCount) return 0;
|
|
874
874
|
const removed = disk.units.length - keepCount;
|
|
875
875
|
disk.units = keepNewestUnits(disk.units, keepCount);
|
|
876
|
-
|
|
876
|
+
saveJsonFileCompact(metricsPath(base), disk);
|
|
877
877
|
// Keep the in-memory ledger in sync if it is loaded for this session.
|
|
878
878
|
if (ledger) {
|
|
879
879
|
ledger.units = keepNewestUnits(ledger.units, keepCount);
|
|
@@ -1041,7 +1041,7 @@ function saveLedger(base: string, data: MetricsLedger): void {
|
|
|
1041
1041
|
: dataUnits;
|
|
1042
1042
|
merged.sort(compareUnitsByTime);
|
|
1043
1043
|
data.units = keepNewestUnits(merged);
|
|
1044
|
-
|
|
1044
|
+
saveJsonFileCompact(path, data);
|
|
1045
1045
|
} finally {
|
|
1046
1046
|
releaseLock(lockPath);
|
|
1047
1047
|
}
|
|
@@ -1053,6 +1053,6 @@ function saveLedger(base: string, data: MetricsLedger): void {
|
|
|
1053
1053
|
// read-merge-write sequence without mutual exclusion.
|
|
1054
1054
|
logWarning("fs", "saveLedger: lock not acquired — falling back to direct write (no merge)");
|
|
1055
1055
|
data.units = keepNewestUnits(data.units);
|
|
1056
|
-
|
|
1056
|
+
saveJsonFileCompact(path, data);
|
|
1057
1057
|
}
|
|
1058
1058
|
}
|
|
@@ -118,6 +118,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
118
118
|
|
|
119
119
|
// Copy contents to external dir, skipping worktrees/
|
|
120
120
|
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
121
|
+
const copyFailures: string[] = [];
|
|
121
122
|
for (const entry of entries) {
|
|
122
123
|
if (entry.name === "worktrees") continue; // worktrees stay local
|
|
123
124
|
|
|
@@ -130,10 +131,15 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
130
131
|
} else {
|
|
131
132
|
cpSync(src, dst, { force: true });
|
|
132
133
|
}
|
|
133
|
-
} catch {
|
|
134
|
-
|
|
134
|
+
} catch (copyErr) {
|
|
135
|
+
copyFailures.push(`${entry.name}: ${getErrorMessage(copyErr)}`);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
if (copyFailures.length > 0) {
|
|
139
|
+
try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
|
|
140
|
+
renameSync(migratingPath, localGsd);
|
|
141
|
+
return { migrated: false, error: `Migration copy failed: ${copyFailures.join("; ")}` };
|
|
142
|
+
}
|
|
137
143
|
|
|
138
144
|
// Create symlink .gsd -> external path
|
|
139
145
|
symlinkSync(externalPath, localGsd, "junction");
|
|
@@ -33,16 +33,87 @@ function _isAlreadyRestoredApplyError(err: unknown): boolean {
|
|
|
33
33
|
|
|
34
34
|
export { _isAlreadyRestoredApplyError };
|
|
35
35
|
|
|
36
|
+
function gitOutput(basePath: string, args: string[]): string {
|
|
37
|
+
return execFileSync("git", args, {
|
|
38
|
+
cwd: basePath,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
env: GIT_NO_PROMPT_ENV,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listStashUntrackedPaths(basePath: string, stashRef: string): string[] | null {
|
|
46
|
+
try {
|
|
47
|
+
return gitOutput(basePath, ["ls-tree", "-r", "-z", "--name-only", `${stashRef}^3`]).split("\0").filter(Boolean);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listStashTrackedPaths(basePath: string, stashRef: string): string[] | null {
|
|
54
|
+
try {
|
|
55
|
+
return gitOutput(basePath, ["diff", "--name-only", "-z", `${stashRef}^1`, stashRef]).split("\0").filter(Boolean);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PorcelainEntry {
|
|
62
|
+
code: string;
|
|
63
|
+
path: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readWorkingTreeStatus(basePath: string): PorcelainEntry[] | null {
|
|
67
|
+
try {
|
|
68
|
+
return gitOutput(basePath, ["status", "--porcelain", "-z", "--untracked-files=all"])
|
|
69
|
+
.split("\0")
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((entry) => ({ code: entry.slice(0, 2), path: entry.slice(3) }));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isAlreadyRestoredUntrackedStatus(basePath: string, stashRef: string, statusEntries: PorcelainEntry[]): boolean {
|
|
78
|
+
if (statusEntries.length === 0 || statusEntries.some((entry) => entry.code !== "??")) return false;
|
|
79
|
+
const stashTrackedPaths = listStashTrackedPaths(basePath, stashRef);
|
|
80
|
+
if (!stashTrackedPaths || stashTrackedPaths.length > 0) return false;
|
|
81
|
+
const stashUntrackedPaths = listStashUntrackedPaths(basePath, stashRef);
|
|
82
|
+
if (!stashUntrackedPaths || stashUntrackedPaths.length === 0) return false;
|
|
83
|
+
const stashUntrackedPathSet = new Set(stashUntrackedPaths);
|
|
84
|
+
const workingTreeUntrackedPathSet = new Set(statusEntries.map((entry) => entry.path));
|
|
85
|
+
// "Already restored" requires the working tree's untracked paths to match the
|
|
86
|
+
// stash's untracked paths exactly, in BOTH directions:
|
|
87
|
+
// 1. every current untracked path comes from the stash (no unexplained user
|
|
88
|
+
// work that an apply would clobber), AND
|
|
89
|
+
// 2. every stash untracked path is already present in the working tree.
|
|
90
|
+
// Without (2), a partial restore (where only some stash files were recovered)
|
|
91
|
+
// would be misread as fully restored: the audit would no-op without applying
|
|
92
|
+
// or warning, leaving the remaining pre-merge files missing.
|
|
93
|
+
return (
|
|
94
|
+
statusEntries.every((entry) => stashUntrackedPathSet.has(entry.path)) &&
|
|
95
|
+
stashUntrackedPaths.every((path) => workingTreeUntrackedPathSet.has(path))
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function manualApplyWarning(stashRef: string, milestoneId: string, reason: string): string {
|
|
100
|
+
return (
|
|
101
|
+
`Pre-merge stash ${stashRef} for milestone ${milestoneId} not auto-applied: ${reason}; ` +
|
|
102
|
+
`run \`git stash apply ${stashRef}\` manually to restore your pre-merge changes.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
36
106
|
/**
|
|
37
107
|
* Audit `git stash list` for orphaned `gsd-preflight-stash:M00x:*` entries.
|
|
38
108
|
*
|
|
39
109
|
* The matching merge code in `phases.ts` previously skipped the postflight
|
|
40
110
|
* pop whenever `mergeAndExit` threw, leaking the user's pre-merge working
|
|
41
|
-
* tree into the stash list.
|
|
42
|
-
*
|
|
111
|
+
* tree into the stash list. Completed-milestone stashes are only auto-applied
|
|
112
|
+
* when the working tree is clean and the stash does not modify tracked files.
|
|
113
|
+
* Dirty trees and tracked-file stashes are left untouched with a manual
|
|
114
|
+
* recovery warning. When auto-apply is safe, `git stash apply` is invoked —
|
|
43
115
|
* NOT `pop`. The stash entry stays in the list so the user retains a backup
|
|
44
|
-
* if the apply produces unexpected merge results.
|
|
45
|
-
* startup runs.
|
|
116
|
+
* if the apply produces unexpected merge results.
|
|
46
117
|
*
|
|
47
118
|
* Failures are best-effort: a list error (no repo, git unavailable) returns
|
|
48
119
|
* an empty result. An apply error becomes a warning the user sees alongside
|
|
@@ -56,16 +127,7 @@ export function auditOrphanedPreflightStashes(
|
|
|
56
127
|
|
|
57
128
|
let listOutput: string;
|
|
58
129
|
try {
|
|
59
|
-
listOutput =
|
|
60
|
-
"git",
|
|
61
|
-
["stash", "list", "--format=%gd%x00%s"],
|
|
62
|
-
{
|
|
63
|
-
cwd: basePath,
|
|
64
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
-
encoding: "utf-8",
|
|
66
|
-
env: GIT_NO_PROMPT_ENV,
|
|
67
|
-
},
|
|
68
|
-
);
|
|
130
|
+
listOutput = gitOutput(basePath, ["stash", "list", "--format=%gd%x00%s"]);
|
|
69
131
|
} catch {
|
|
70
132
|
return result;
|
|
71
133
|
}
|
|
@@ -93,13 +155,39 @@ export function auditOrphanedPreflightStashes(
|
|
|
93
155
|
}
|
|
94
156
|
if (!complete) continue;
|
|
95
157
|
|
|
158
|
+
const statusEntries = readWorkingTreeStatus(basePath);
|
|
159
|
+
if (!statusEntries) {
|
|
160
|
+
result.warnings.push(
|
|
161
|
+
manualApplyWarning(ref, milestoneId, "could not verify working tree status"),
|
|
162
|
+
);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (statusEntries.length > 0) {
|
|
166
|
+
if (isAlreadyRestoredUntrackedStatus(basePath, ref, statusEntries)) continue;
|
|
167
|
+
result.warnings.push(manualApplyWarning(ref, milestoneId, "working tree not clean"));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const trackedPaths = listStashTrackedPaths(basePath, ref);
|
|
172
|
+
if (trackedPaths === null) {
|
|
173
|
+
result.warnings.push(
|
|
174
|
+
manualApplyWarning(ref, milestoneId, "could not inspect tracked paths in stash"),
|
|
175
|
+
);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (trackedPaths.length > 0) {
|
|
179
|
+
// A retained tracked-file stash has no durable marker showing whether a
|
|
180
|
+
// clean tree needs first recovery or would be re-mutated by an old backup.
|
|
181
|
+
const preview = trackedPaths.slice(0, 5).join(", ");
|
|
182
|
+
const suffix = trackedPaths.length > 5 ? `, and ${trackedPaths.length - 5} more` : "";
|
|
183
|
+
result.warnings.push(
|
|
184
|
+
manualApplyWarning(ref, milestoneId, `stash modifies tracked files (${preview}${suffix})`),
|
|
185
|
+
);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
96
189
|
try {
|
|
97
|
-
|
|
98
|
-
cwd: basePath,
|
|
99
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
100
|
-
encoding: "utf-8",
|
|
101
|
-
env: GIT_NO_PROMPT_ENV,
|
|
102
|
-
});
|
|
190
|
+
gitOutput(basePath, ["stash", "apply", "--quiet", ref]);
|
|
103
191
|
result.applied.push({ milestoneId, stashRef: ref });
|
|
104
192
|
} catch (err) {
|
|
105
193
|
// Idempotent steady state: stash was already applied in a prior audit
|
|
@@ -104,8 +104,8 @@ If the preference is off, ignore this section and plan every slice fully.
|
|
|
104
104
|
|
|
105
105
|
If the roadmap has one slice, also plan S01 and its tasks inline:
|
|
106
106
|
|
|
107
|
-
1. After `gsd_plan_milestone` returns, call `gsd_plan_slice` for S01 with
|
|
108
|
-
2. Use inlined **Slice Plan** and **Task Plan** templates for tool parameters.
|
|
107
|
+
1. After `gsd_plan_milestone` returns, call `gsd_plan_slice` for S01 with slice metadata only; omit `tasks`.
|
|
108
|
+
2. Then call `gsd_plan_task` once per S01 task. Use inlined **Slice Plan** and **Task Plan** templates for tool parameters.
|
|
109
109
|
3. Keep simple slices lean. Omit Proof Level, Integration Closure, and Observability if all would be "none"; executable verification commands are enough.
|
|
110
110
|
|
|
111
111
|
Do **not** write plan files manually; use DB-backed tools so state stays consistent.
|
|
@@ -45,8 +45,8 @@ If slice research is inlined, trust its architectural findings, but verify every
|
|
|
45
45
|
6. Include Threat Surface (Q3), Requirement Impact (Q4), proof level, observability, integration closure, Failure Modes (Q5), Load Profile (Q6), and Negative Tests (Q7) only where applicable.
|
|
46
46
|
7. Right-size tasks. Simple slices can be one task; split only when context, ownership, or verification boundaries justify it.
|
|
47
47
|
8. Task `verify` commands must be safe, simple commands. Do not use shell pipes, redirects, semicolons, backticks, command substitution, output trimming, or grep regex alternation with `|`. If multiple checks are needed, create a small test file and run it with `node --test` or a package test script, or use separate simple commands joined only with `&&`. For absence checks, verify a pattern does not exist with `! grep -q 'pattern' file` or `! rg -q 'pattern' file`; do not use `grep -c` or `rg -c` to assert zero matches because count commands exit 1 when they find zero matches, and the verification gate treats that as failure.
|
|
48
|
-
9. Each task needs the exact
|
|
49
|
-
10. Persist
|
|
48
|
+
9. Each task persisted with `gsd_plan_task` needs the exact shape: `milestoneId`, `sliceId`, `taskId`, `title`, `description`, `estimate`, `files`, `verify`, `inputs`, `expectedOutput`, and optional `observabilityImpact`. `description` should contain the Why / Do / Done-when narrative. Any npm package name in a task description's install/add instructions (`npm install`, `yarn add`, `pnpm add`) or in `require('...')` or `import ... from '...'` code examples is subject to pre-execution registry validation; reference only packages that exist on the public npm registry, and remove or correct any package name that is not real. `files`, `inputs`, and `expectedOutput` must be JSON arrays of strings, even when there is only one path (for example, `"inputs": ["src/index.ts"]`, never `"inputs": "src/index.ts"`). Use paths relative to `{{workingDirectory}}`; do not put absolute paths to the original checkout or any directory outside `{{workingDirectory}}` in `files`, `inputs`, `expectedOutput`, or verification commands. **`expectedOutput` must only list files the task actually creates or overwrites on disk.** Do NOT include files the task merely reads, verifies, tests, or describes — those belong in `inputs`, `verify`, `description`, or slice success criteria. If a task is a pure verification or test task that produces no new files, `expectedOutput` must be `[]`; if it writes a test-result log or assertion output file, list only that concrete file path. A file that does not yet exist on disk and is needed as an `input` must be produced by an earlier task's `expectedOutput` — if no prior task creates it, add a task before this one that does. Never list GSD planning artifacts — anything under `.gsd/`, `.planning/`, or `.audits/`, or artifact names like `M001-CONTEXT.md` / `S01-PLAN.md` — in `inputs`, `files`, or `expectedOutput`: their content is preloaded as context, and they are written by workflow tools, not by tasks.
|
|
49
|
+
10. Persist incrementally through DB-backed tools. First call `gsd_plan_slice` with `milestoneId`, `sliceId`, `goal`, and optional `successCriteria`/`proofLevel`/`integrationClosure`/`observabilityImpact`; omit `tasks` for this metadata call. Then call `gsd_plan_task` once per task, using the task shape above. Each successful tool call saves progress and re-renders `{{outputPath}}` from DB. The DB-backed tools are the canonical write path. Do **not** rely on direct `PLAN.md` writes as the source of truth.
|
|
50
50
|
11. Self-audit before finishing: goal/demo closure, requirement coverage, deliverable coverage audit (cross-check every file listed in CONTEXT.md `## Scope` / `### In Scope` against task `files` or `expectedOutput`), locked decisions, concrete paths, dependency order, wiring, scope size, proof truthfulness, feature completeness, and quality gates. Quality gates: non-trivial slices/tasks include specific Q3-Q7 coverage where applicable.
|
|
51
51
|
12. If planning creates structural decisions, call `gsd_decision_save` for each; the tool persists the decision and regenerates `.gsd/DECISIONS.md`.
|
|
52
52
|
13. {{commitInstruction}}
|
|
@@ -55,6 +55,6 @@ The slice directory already exists. Do not mkdir.
|
|
|
55
55
|
|
|
56
56
|
**Autonomous execution:** no human is available. Do not call `ask_user_questions` or `secure_env_collect`; make reasonable assumptions and document them.
|
|
57
57
|
|
|
58
|
-
**You MUST call `gsd_plan_slice`
|
|
58
|
+
**You MUST call `gsd_plan_slice` and then `gsd_plan_task` once for each planned task before finishing, unless the stale-path safety rule above stops the unit before safe planning can occur.**
|
|
59
59
|
|
|
60
60
|
When done, say: "Slice {{sliceId}} planned." Say this exactly once — if you already said it in a prior message, do not repeat it.
|
|
@@ -5,8 +5,18 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { GSD_RUNTIME_PATTERNS } from "./gitignore.js";
|
|
8
9
|
|
|
9
10
|
const MAX_SYNC_FINGERPRINT_BYTES = 1_500_000_000;
|
|
11
|
+
const ROOT_RUNTIME_PATH_PREFIXES = Array.from(
|
|
12
|
+
new Set(
|
|
13
|
+
GSD_RUNTIME_PATTERNS.map((pattern) => {
|
|
14
|
+
const path = pattern.replace(/\/+$/, "");
|
|
15
|
+
const slash = path.indexOf("/");
|
|
16
|
+
return slash === -1 ? path : path.slice(0, slash);
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
);
|
|
10
20
|
|
|
11
21
|
export interface RootDirtyEntry {
|
|
12
22
|
path: string;
|
|
@@ -25,7 +35,9 @@ export interface RootWriteLeak {
|
|
|
25
35
|
export type RootDirtySnapshot = Map<string, RootDirtyEntry>;
|
|
26
36
|
|
|
27
37
|
function isRootRuntimePath(path: string): boolean {
|
|
28
|
-
return
|
|
38
|
+
return ROOT_RUNTIME_PATH_PREFIXES.some(
|
|
39
|
+
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
|
40
|
+
);
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
function fileFingerprint(rootPath: string, relPath: string): string {
|
|
@@ -29,6 +29,13 @@ const DESTRUCTIVE_PATTERNS: readonly DestructivePattern[] = [
|
|
|
29
29
|
{ pattern: /\bkubectl\s+(delete|apply)\b/i, label: "kubectl mutation" },
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
+
function stripQuotedAndComments(command: string): string {
|
|
33
|
+
return command
|
|
34
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
35
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
|
|
36
|
+
.replace(/(^|\s)#.*$/g, "$1");
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
33
40
|
|
|
34
41
|
export interface CommandClassification {
|
|
@@ -41,9 +48,10 @@ export interface CommandClassification {
|
|
|
41
48
|
* Returns the list of matched destructive pattern labels.
|
|
42
49
|
*/
|
|
43
50
|
export function classifyCommand(command: string): CommandClassification {
|
|
51
|
+
const commandToClassify = stripQuotedAndComments(command);
|
|
44
52
|
const labels: string[] = [];
|
|
45
53
|
for (const { pattern, label } of DESTRUCTIVE_PATTERNS) {
|
|
46
|
-
if (pattern.test(
|
|
54
|
+
if (pattern.test(commandToClassify)) {
|
|
47
55
|
// Deduplicate labels (e.g., two force-push patterns)
|
|
48
56
|
if (!labels.includes(label)) labels.push(label);
|
|
49
57
|
}
|
|
@@ -65,12 +65,16 @@ const MCP_EXECUTION_TOOL_RE = /^mcp__.+__gsd_(?:uat_)?exec(?:_search)?$/;
|
|
|
65
65
|
// ─── Module State ───────────────────────────────────────────────────────────
|
|
66
66
|
|
|
67
67
|
let unitEvidence: EvidenceEntry[] = [];
|
|
68
|
+
let lastWritePath: string | null = null;
|
|
69
|
+
let lastWriteSig: string | null = null;
|
|
68
70
|
|
|
69
71
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
70
72
|
|
|
71
73
|
/** Reset all evidence for a new unit. Call at unit start. */
|
|
72
74
|
export function resetEvidence(): void {
|
|
73
75
|
unitEvidence = [];
|
|
76
|
+
lastWritePath = null;
|
|
77
|
+
lastWriteSig = null;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/** Get a read-only view of all evidence collected for the current unit. */
|
|
@@ -145,10 +149,15 @@ export function saveEvidenceToDisk(
|
|
|
145
149
|
): void {
|
|
146
150
|
try {
|
|
147
151
|
const path = evidencePath(basePath, milestoneId, sliceId, taskId);
|
|
152
|
+
const body = JSON.stringify(unitEvidence, null, 2) + "\n";
|
|
153
|
+
if (path === lastWritePath && body === lastWriteSig) return;
|
|
154
|
+
|
|
148
155
|
mkdirSync(dirname(path), { recursive: true });
|
|
149
156
|
const tmp = `${path}.tmp.${randomBytes(4).toString("hex")}`;
|
|
150
|
-
writeFileSync(tmp,
|
|
157
|
+
writeFileSync(tmp, body, "utf-8");
|
|
151
158
|
renameSync(tmp, path);
|
|
159
|
+
lastWritePath = path;
|
|
160
|
+
lastWriteSig = body;
|
|
152
161
|
} catch {
|
|
153
162
|
// Non-fatal — don't let persistence failures break unit execution
|
|
154
163
|
}
|
|
@@ -167,6 +176,8 @@ export function loadEvidenceFromDisk(
|
|
|
167
176
|
taskId: string,
|
|
168
177
|
): void {
|
|
169
178
|
try {
|
|
179
|
+
lastWritePath = null;
|
|
180
|
+
lastWriteSig = null;
|
|
170
181
|
const path = evidencePath(basePath, milestoneId, sliceId, taskId);
|
|
171
182
|
if (!existsSync(path)) return;
|
|
172
183
|
const raw = readFileSync(path, "utf-8");
|
|
@@ -195,6 +206,10 @@ export function clearEvidenceFromDisk(
|
|
|
195
206
|
if (existsSync(path)) {
|
|
196
207
|
unlinkSync(path);
|
|
197
208
|
}
|
|
209
|
+
if (path === lastWritePath) {
|
|
210
|
+
lastWritePath = null;
|
|
211
|
+
lastWriteSig = null;
|
|
212
|
+
}
|
|
198
213
|
} catch {
|
|
199
214
|
// Non-fatal
|
|
200
215
|
}
|
|
@@ -76,8 +76,14 @@ export function validateFileChanges(
|
|
|
76
76
|
const actualFiles = getChangedFilesFromLastCommit(basePath);
|
|
77
77
|
if (!actualFiles) return null;
|
|
78
78
|
|
|
79
|
-
// Filter out
|
|
80
|
-
const projectFiles = actualFiles.filter(
|
|
79
|
+
// Filter out GSD-internal paths — only validate project source files.
|
|
80
|
+
const projectFiles = actualFiles.filter(
|
|
81
|
+
(f) =>
|
|
82
|
+
!f.startsWith(".gsd/") &&
|
|
83
|
+
!f.startsWith(".gsd\\") &&
|
|
84
|
+
!f.startsWith(".gsd-backups/") &&
|
|
85
|
+
!f.startsWith(".gsd-backups\\"),
|
|
86
|
+
);
|
|
81
87
|
|
|
82
88
|
// Normalize expected paths (strip leading ./ or /)
|
|
83
89
|
const normalizedExpected = new Set(
|
|
@@ -29,21 +29,22 @@ test("#2007 bug 1: MAX_ARTIFACT_VERIFICATION_RETRIES constant is defined", () =>
|
|
|
29
29
|
assert.equal(MAX_ARTIFACT_VERIFICATION_RETRIES, 3);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test("#2007 bug 1: retry state
|
|
32
|
+
test("#2007 bug 1: retry state keeps attempt separate from failure context", () => {
|
|
33
33
|
const s = new AutoSession();
|
|
34
34
|
const retryKey = "execute-task:M001/S01/T01";
|
|
35
|
+
const failureContext = "Missing expected artifact";
|
|
35
36
|
|
|
36
37
|
for (let attempt = 1; attempt <= MAX_ARTIFACT_VERIFICATION_RETRIES; attempt++) {
|
|
37
38
|
s.verificationRetryCount.set(retryKey, attempt);
|
|
38
39
|
s.pendingVerificationRetry = {
|
|
39
40
|
unitId: "M001/S01/T01",
|
|
40
|
-
failureContext
|
|
41
|
+
failureContext,
|
|
41
42
|
attempt,
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
assert.equal(s.verificationRetryCount.get(retryKey), attempt);
|
|
45
46
|
assert.equal(s.pendingVerificationRetry.attempt, attempt);
|
|
46
|
-
assert.
|
|
47
|
+
assert.equal(s.pendingVerificationRetry.failureContext, failureContext);
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
|