@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.965d1788
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +5 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +6 -0
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
- package/dist/resources/extensions/gsd/git-service.js +1 -0
- package/dist/resources/extensions/gsd/gitignore.js +3 -0
- 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/orphan-stash-audit.js +92 -16
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -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/undo.js +43 -6
- package/dist/resources/extensions/gsd/verification-gate.js +7 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +4 -1
- 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 +10 -10
- 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 +10 -10
- 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/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +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/gsd/auto/session.ts +1 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +5 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -0
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
- package/src/resources/extensions/gsd/git-service.ts +1 -0
- package/src/resources/extensions/gsd/gitignore.ts +3 -0
- 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/orphan-stash-audit.ts +108 -20
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -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/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/decisions-projection-from-memories.test.ts +51 -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/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/orphan-stash-audit.test.ts +91 -7
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +54 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
- package/src/resources/extensions/gsd/undo.ts +47 -6
- package/src/resources/extensions/gsd/verification-gate.ts +8 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ Need help choosing settings? Use the [GSD Pi web configurator](https://pi.opengs
|
|
|
158
158
|
gsd
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Run the setup flow, choose your preferred model provider, and open a project directory. GSD stores project planning and runtime state in `.gsd
|
|
161
|
+
Run the setup flow, choose your preferred model provider, and open a project directory. GSD stores project planning and runtime state in `.gsd/`, with gitignored sibling runtime directories such as `.gsd-backups/` for migration snapshots. Stale `.gsd-backups/migrate-*` snapshots are pruned after 30 days once the flat-phase `.gsd/phases/` migration is complete.
|
|
162
162
|
|
|
163
163
|
For a full first-run walkthrough, see [Getting Started With gsd-pi](./docs/user-docs/getting-started.md).
|
|
164
164
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
377a5b3cd6318ed2
|
|
@@ -25,7 +25,7 @@ export function decideVerificationRetry(input) {
|
|
|
25
25
|
return { action: "pause", reason: "missing-retry-context" };
|
|
26
26
|
}
|
|
27
27
|
const key = verificationRetryKey(unitType, retryInfo.unitId);
|
|
28
|
-
const failureHash = hashVerificationFailureContext(retryInfo.failureContext);
|
|
28
|
+
const failureHash = hashVerificationFailureContext(retryInfo.signature ?? retryInfo.failureContext);
|
|
29
29
|
if (input.previousFailureHash === failureHash) {
|
|
30
30
|
return {
|
|
31
31
|
action: "pause",
|
|
@@ -1834,7 +1834,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
1834
1834
|
saveCustomVerifyRetryCounts(s, { logFailure: err => debugLog("postUnit", { phase: "save-verify-retries-failed", error: err instanceof Error ? err.message : String(err) }) });
|
|
1835
1835
|
s.pendingVerificationRetry = {
|
|
1836
1836
|
unitId: s.currentUnit.id,
|
|
1837
|
-
failureContext:
|
|
1837
|
+
failureContext: failureDetails,
|
|
1838
1838
|
attempt,
|
|
1839
1839
|
};
|
|
1840
1840
|
debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
|
|
@@ -9,7 +9,7 @@ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices } from "./gsd
|
|
|
9
9
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
10
10
|
import { isClosedStatus } from "./status-guards.js";
|
|
11
11
|
import { loadFile } from "./files.js";
|
|
12
|
-
import { runVerificationGate, runVerificationGateForTargets, formatFailureContext, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
|
|
12
|
+
import { runVerificationGate, runVerificationGateForTargets, formatFailureContext, formatFailureSignature, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
|
|
13
13
|
import { writeVerificationJSON } from "./verification-evidence.js";
|
|
14
14
|
import { logWarning } from "./workflow-logger.js";
|
|
15
15
|
import { runPostExecutionChecks } from "./post-execution-checks.js";
|
|
@@ -686,10 +686,13 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
686
686
|
ctx.ui.notify(`Unit ${s.currentUnit.id} cost spike detected (${unitCostUsd.toFixed(2)} vs avg ${rollingAvgUsd.toFixed(2)}) during verification retry; keeping verification failure as the authoritative blocker.`, "warning");
|
|
687
687
|
}
|
|
688
688
|
const nextAttempt = attempt + 1;
|
|
689
|
+
const failureContext = verdict.failureContext || formatFailureContext(result);
|
|
690
|
+
const failureSignature = formatFailureSignature(result);
|
|
689
691
|
s.verificationRetryCount.set(retryKey, nextAttempt);
|
|
690
692
|
s.pendingVerificationRetry = {
|
|
691
693
|
unitId: s.currentUnit.id,
|
|
692
|
-
failureContext
|
|
694
|
+
failureContext,
|
|
695
|
+
...(failureSignature ? { signature: failureSignature } : {}),
|
|
693
696
|
attempt: nextAttempt,
|
|
694
697
|
};
|
|
695
698
|
const failedCmds = result.checks
|
|
@@ -703,6 +703,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
|
+
const projectRoot = resolveWorktreeProjectRoot(basePath);
|
|
707
|
+
const { pruneStaleFlatPhaseBackups } = await import("../flat-phase-migration.js");
|
|
708
|
+
const pruned = pruneStaleFlatPhaseBackups(projectRoot);
|
|
709
|
+
if (pruned > 0) {
|
|
710
|
+
safetyLogWarning("bootstrap", `pruned ${pruned} stale flat-phase migration backup(s) from .gsd-backups/ (retention exceeded)`);
|
|
711
|
+
}
|
|
706
712
|
}
|
|
707
713
|
catch (err) {
|
|
708
714
|
safetyLogWarning("bootstrap", `flat-phase migration: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getLatestAssessmentByScope, getMilestone, getMilestoneSlices, getPendingGates, getSliceTasks, insertAssessment, isDbAvailable, transaction, } from "./gsd-db.js";
|
|
6
6
|
import { getWorkflowDatabasePath, refreshWorkflowDatabaseFromDisk, } from "./db-workspace.js";
|
|
7
|
-
import { isClosedStatus } from "./status-guards.js";
|
|
7
|
+
import { isClosedStatus, isDeferredStatus } from "./status-guards.js";
|
|
8
8
|
import { closeQualityGatesFromEvidence } from "./quality-gate-closure.js";
|
|
9
9
|
import { insertMilestoneValidationGates } from "./milestone-validation-gates.js";
|
|
10
10
|
import { relMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
@@ -144,6 +144,8 @@ export function checkCloseoutConsistencyGate(milestoneId, options = {}) {
|
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
for (const slice of slices) {
|
|
147
|
+
if (isDeferredStatus(slice.status))
|
|
148
|
+
continue;
|
|
147
149
|
if (!isClosedStatus(slice.status)) {
|
|
148
150
|
return blocked("slice-open", `Closeout consistency blocked for ${milestoneId}: slice ${slice.id} status is "${slice.status}".`);
|
|
149
151
|
}
|
|
@@ -387,6 +387,9 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
387
387
|
if (!existingLines.has(".gsd-worktrees/") && !existingLines.has(".gsd-worktrees")) {
|
|
388
388
|
missing.push(".gsd-worktrees/");
|
|
389
389
|
}
|
|
390
|
+
if (!existingLines.has(".gsd-backups/") && !existingLines.has(".gsd-backups")) {
|
|
391
|
+
missing.push(".gsd-backups/");
|
|
392
|
+
}
|
|
390
393
|
if (!hasBlanketIgnore) {
|
|
391
394
|
missing.push(...criticalPatterns.filter(p => !existingLines.has(p)));
|
|
392
395
|
}
|
|
@@ -59,6 +59,51 @@ export function needsFlatPhaseMigration(basePath) {
|
|
|
59
59
|
// Resume an interrupted migration where the legacy tree was renamed aside.
|
|
60
60
|
return hasLegacyMilestoneSubdirs(legacyMigratingPath(basePath));
|
|
61
61
|
}
|
|
62
|
+
/** Retention window before flat-phase migration backups are auto-pruned. */
|
|
63
|
+
export const FLAT_PHASE_BACKUP_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
64
|
+
/**
|
|
65
|
+
* Remove stale flat-phase migration backups after the retention window.
|
|
66
|
+
* Only runs when migration is complete (.gsd/phases/ exists, legacy layout gone).
|
|
67
|
+
* Returns the number of migrate-* directories removed.
|
|
68
|
+
*/
|
|
69
|
+
export function pruneStaleFlatPhaseBackups(basePath) {
|
|
70
|
+
if (needsFlatPhaseMigration(basePath))
|
|
71
|
+
return 0;
|
|
72
|
+
const phasesPath = join(basePath, ".gsd", LAYOUT_SEGMENTS.level1);
|
|
73
|
+
if (!existsSync(phasesPath))
|
|
74
|
+
return 0;
|
|
75
|
+
const backupRoot = join(basePath, ".gsd-backups");
|
|
76
|
+
if (!existsSync(backupRoot))
|
|
77
|
+
return 0;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
let removed = 0;
|
|
80
|
+
for (const entry of readdirSync(backupRoot)) {
|
|
81
|
+
if (!entry.startsWith("migrate-"))
|
|
82
|
+
continue;
|
|
83
|
+
const dirPath = join(backupRoot, entry);
|
|
84
|
+
try {
|
|
85
|
+
const st = statSync(dirPath);
|
|
86
|
+
if (!st.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
if (now - st.mtimeMs < FLAT_PHASE_BACKUP_RETENTION_MS)
|
|
89
|
+
continue;
|
|
90
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
91
|
+
removed++;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Non-fatal: leave backup for manual recovery.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
if (readdirSync(backupRoot).length === 0) {
|
|
99
|
+
rmSync(backupRoot, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Non-fatal.
|
|
104
|
+
}
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
62
107
|
/**
|
|
63
108
|
* Migrate from legacy nested .gsd/milestones/ to flat-phase .gsd/phases/.
|
|
64
109
|
*
|
|
@@ -24,6 +24,7 @@ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
|
24
24
|
*/
|
|
25
25
|
const GSD_RUNTIME_PATTERNS = [
|
|
26
26
|
".gsd-worktrees/",
|
|
27
|
+
".gsd-backups/",
|
|
27
28
|
".gsd/activity/",
|
|
28
29
|
".gsd/audit/",
|
|
29
30
|
".gsd/forensics/",
|
|
@@ -48,6 +49,8 @@ const BASELINE_PATTERNS = [
|
|
|
48
49
|
".gsd",
|
|
49
50
|
// Worktree container sibling — NOT covered by the ".gsd" pattern above.
|
|
50
51
|
".gsd-worktrees/",
|
|
52
|
+
// Flat-phase migration snapshots — NOT covered by the ".gsd" pattern above.
|
|
53
|
+
".gsd-backups/",
|
|
51
54
|
".gsd-id",
|
|
52
55
|
".mcp.json",
|
|
53
56
|
".bg-shell/",
|
|
@@ -56,6 +56,26 @@ export function saveJsonFile(filePath, data) {
|
|
|
56
56
|
// Non-fatal — don't let persistence failures break operation
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Save a JSON file atomically using compact serialization.
|
|
61
|
+
*
|
|
62
|
+
* Intended for machine-only ledgers where pretty-printing has no consumer.
|
|
63
|
+
* Keeps the same write-tmp-rename safety and non-fatal error behavior as
|
|
64
|
+
* saveJsonFile.
|
|
65
|
+
*/
|
|
66
|
+
export function saveJsonFileCompact(filePath, data) {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
69
|
+
// Use randomized tmp suffix to prevent concurrent-write data loss
|
|
70
|
+
const tmp = `${filePath}.tmp.${randomBytes(4).toString("hex")}`;
|
|
71
|
+
writeFileSync(tmp, JSON.stringify(data) + "\n", "utf-8");
|
|
72
|
+
renameSync(tmp, filePath);
|
|
73
|
+
// No cleanup needed — renameSync atomically removes tmp on success
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Non-fatal — don't let persistence failures break operation
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
/**
|
|
60
80
|
* Write a JSON file atomically (write to .tmp, then rename).
|
|
61
81
|
* Creates parent directories as needed. Non-fatal on error.
|
|
@@ -24,8 +24,8 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
24
24
|
* - Idempotent (per-row): each migrated memory carries
|
|
25
25
|
* `structured_fields.sourceKnowledgeId = "<P|L>NNN"`. Rows whose ID is
|
|
26
26
|
* already present in the memory store are skipped.
|
|
27
|
-
* - Best-effort: never throws.
|
|
28
|
-
* backfill cannot block
|
|
27
|
+
* - Best-effort: never throws. Failed memory inserts are logged and skipped;
|
|
28
|
+
* setup/read failures log and return 0 so backfill cannot block startup.
|
|
29
29
|
* - Rules (K###) are intentionally skipped — they remain manually maintained
|
|
30
30
|
* in `KNOWLEDGE.md` per ADR-013.
|
|
31
31
|
*
|
|
@@ -56,15 +56,22 @@ export function backfillKnowledgeToMemories(basePath) {
|
|
|
56
56
|
const matchPattern = `%"sourceKnowledgeId":"${synth.id}"%`;
|
|
57
57
|
if (checkExisting.get({ ":pattern": matchPattern }))
|
|
58
58
|
continue;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
try {
|
|
60
|
+
const id = createMemory({
|
|
61
|
+
category: synth.category,
|
|
62
|
+
content: synth.content,
|
|
63
|
+
scope: synth.scope,
|
|
64
|
+
confidence: 0.85,
|
|
65
|
+
structuredFields: synth.structuredFields,
|
|
66
|
+
});
|
|
67
|
+
if (id)
|
|
68
|
+
written += 1;
|
|
69
|
+
}
|
|
70
|
+
catch (rowErr) {
|
|
71
|
+
const message = rowErr instanceof Error ? rowErr.message : String(rowErr);
|
|
72
|
+
logWarning("knowledge-backfill", `KNOWLEDGE.md -> memories backfill skipped ${synth.id}: ${message}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
return written;
|
|
70
77
|
}
|
|
@@ -33,8 +33,8 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
33
33
|
* decision is checked individually; only decisions whose id is already
|
|
34
34
|
* present in the memory store are skipped. A user-authored memory with
|
|
35
35
|
* their own `sourceDecisionId` does NOT abort the backfill.
|
|
36
|
-
* - Best-effort: never throws.
|
|
37
|
-
* backfill cannot block
|
|
36
|
+
* - Best-effort: never throws. Failed memory inserts are logged and skipped;
|
|
37
|
+
* setup/read failures log and return 0 so backfill cannot block startup.
|
|
38
38
|
* - Full migration (ADR-013 Stage 2a): both active and superseded rows are
|
|
39
39
|
* migrated. `structured_fields.superseded_by` preserves the supersedes
|
|
40
40
|
* chain so the DECISIONS.md projection regen can source from memories
|
|
@@ -117,25 +117,32 @@ export function backfillDecisionsToMemories() {
|
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
119
|
const content = synthesizeDecisionMemoryContent(row);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
try {
|
|
121
|
+
const id = createMemory({
|
|
122
|
+
category: "architecture",
|
|
123
|
+
content,
|
|
124
|
+
scope: row.scope || "project",
|
|
125
|
+
confidence: 0.85,
|
|
126
|
+
structuredFields: {
|
|
127
|
+
sourceDecisionId: row.id,
|
|
128
|
+
when_context: row.when_context,
|
|
129
|
+
scope: row.scope,
|
|
130
|
+
decision: row.decision,
|
|
131
|
+
choice: row.choice,
|
|
132
|
+
rationale: row.rationale,
|
|
133
|
+
made_by: row.made_by,
|
|
134
|
+
revisable: row.revisable,
|
|
135
|
+
superseded_by: row.superseded_by,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
if (id)
|
|
139
|
+
written += 1;
|
|
140
|
+
}
|
|
141
|
+
catch (rowErr) {
|
|
142
|
+
const message = rowErr instanceof Error ? rowErr.message : String(rowErr);
|
|
143
|
+
logWarning("memory-backfill", `decisions->memories backfill skipped ${row.id}: ${message}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
139
146
|
}
|
|
140
147
|
if (healed > 0) {
|
|
141
148
|
logWarning("memory-backfill", `decisions->memories drift healed: ${healed} row${healed === 1 ? "" : "s"} updated superseded_by`);
|
|
@@ -17,7 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { openSync, closeSync, unlinkSync, statSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { gsdRoot } from "./paths.js";
|
|
19
19
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
20
|
-
import { loadJsonFile, loadJsonFileOrNull,
|
|
20
|
+
import { loadJsonFile, loadJsonFileOrNull, saveJsonFileCompact } from "./json-persistence.js";
|
|
21
21
|
import { parseUnitId } from "./unit-id.js";
|
|
22
22
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
23
23
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
@@ -636,7 +636,7 @@ export function pruneMetricsLedger(base, keepCount) {
|
|
|
636
636
|
return 0;
|
|
637
637
|
const removed = disk.units.length - keepCount;
|
|
638
638
|
disk.units = keepNewestUnits(disk.units, keepCount);
|
|
639
|
-
|
|
639
|
+
saveJsonFileCompact(metricsPath(base), disk);
|
|
640
640
|
// Keep the in-memory ledger in sync if it is loaded for this session.
|
|
641
641
|
if (ledger) {
|
|
642
642
|
ledger.units = keepNewestUnits(ledger.units, keepCount);
|
|
@@ -799,7 +799,7 @@ function saveLedger(base, data) {
|
|
|
799
799
|
: dataUnits;
|
|
800
800
|
merged.sort(compareUnitsByTime);
|
|
801
801
|
data.units = keepNewestUnits(merged);
|
|
802
|
-
|
|
802
|
+
saveJsonFileCompact(path, data);
|
|
803
803
|
}
|
|
804
804
|
finally {
|
|
805
805
|
releaseLock(lockPath);
|
|
@@ -813,6 +813,6 @@ function saveLedger(base, data) {
|
|
|
813
813
|
// read-merge-write sequence without mutual exclusion.
|
|
814
814
|
logWarning("fs", "saveLedger: lock not acquired — falling back to direct write (no merge)");
|
|
815
815
|
data.units = keepNewestUnits(data.units);
|
|
816
|
-
|
|
816
|
+
saveJsonFileCompact(path, data);
|
|
817
817
|
}
|
|
818
818
|
}
|
|
@@ -26,16 +26,78 @@ function _isAlreadyRestoredApplyError(err) {
|
|
|
26
26
|
return /already exists, no checkout/i.test(message);
|
|
27
27
|
}
|
|
28
28
|
export { _isAlreadyRestoredApplyError };
|
|
29
|
+
function gitOutput(basePath, args) {
|
|
30
|
+
return execFileSync("git", args, {
|
|
31
|
+
cwd: basePath,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
env: GIT_NO_PROMPT_ENV,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function listStashUntrackedPaths(basePath, stashRef) {
|
|
38
|
+
try {
|
|
39
|
+
return gitOutput(basePath, ["ls-tree", "-r", "-z", "--name-only", `${stashRef}^3`]).split("\0").filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function listStashTrackedPaths(basePath, stashRef) {
|
|
46
|
+
try {
|
|
47
|
+
return gitOutput(basePath, ["diff", "--name-only", "-z", `${stashRef}^1`, stashRef]).split("\0").filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function readWorkingTreeStatus(basePath) {
|
|
54
|
+
try {
|
|
55
|
+
return gitOutput(basePath, ["status", "--porcelain", "-z", "--untracked-files=all"])
|
|
56
|
+
.split("\0")
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((entry) => ({ code: entry.slice(0, 2), path: entry.slice(3) }));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isAlreadyRestoredUntrackedStatus(basePath, stashRef, statusEntries) {
|
|
65
|
+
if (statusEntries.length === 0 || statusEntries.some((entry) => entry.code !== "??"))
|
|
66
|
+
return false;
|
|
67
|
+
const stashTrackedPaths = listStashTrackedPaths(basePath, stashRef);
|
|
68
|
+
if (!stashTrackedPaths || stashTrackedPaths.length > 0)
|
|
69
|
+
return false;
|
|
70
|
+
const stashUntrackedPaths = listStashUntrackedPaths(basePath, stashRef);
|
|
71
|
+
if (!stashUntrackedPaths || stashUntrackedPaths.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
const stashUntrackedPathSet = new Set(stashUntrackedPaths);
|
|
74
|
+
const workingTreeUntrackedPathSet = new Set(statusEntries.map((entry) => entry.path));
|
|
75
|
+
// "Already restored" requires the working tree's untracked paths to match the
|
|
76
|
+
// stash's untracked paths exactly, in BOTH directions:
|
|
77
|
+
// 1. every current untracked path comes from the stash (no unexplained user
|
|
78
|
+
// work that an apply would clobber), AND
|
|
79
|
+
// 2. every stash untracked path is already present in the working tree.
|
|
80
|
+
// Without (2), a partial restore (where only some stash files were recovered)
|
|
81
|
+
// would be misread as fully restored: the audit would no-op without applying
|
|
82
|
+
// or warning, leaving the remaining pre-merge files missing.
|
|
83
|
+
return (statusEntries.every((entry) => stashUntrackedPathSet.has(entry.path)) &&
|
|
84
|
+
stashUntrackedPaths.every((path) => workingTreeUntrackedPathSet.has(path)));
|
|
85
|
+
}
|
|
86
|
+
function manualApplyWarning(stashRef, milestoneId, reason) {
|
|
87
|
+
return (`Pre-merge stash ${stashRef} for milestone ${milestoneId} not auto-applied: ${reason}; ` +
|
|
88
|
+
`run \`git stash apply ${stashRef}\` manually to restore your pre-merge changes.`);
|
|
89
|
+
}
|
|
29
90
|
/**
|
|
30
91
|
* Audit `git stash list` for orphaned `gsd-preflight-stash:M00x:*` entries.
|
|
31
92
|
*
|
|
32
93
|
* The matching merge code in `phases.ts` previously skipped the postflight
|
|
33
94
|
* pop whenever `mergeAndExit` threw, leaking the user's pre-merge working
|
|
34
|
-
* tree into the stash list.
|
|
35
|
-
*
|
|
95
|
+
* tree into the stash list. Completed-milestone stashes are only auto-applied
|
|
96
|
+
* when the working tree is clean and the stash does not modify tracked files.
|
|
97
|
+
* Dirty trees and tracked-file stashes are left untouched with a manual
|
|
98
|
+
* recovery warning. When auto-apply is safe, `git stash apply` is invoked —
|
|
36
99
|
* NOT `pop`. The stash entry stays in the list so the user retains a backup
|
|
37
|
-
* if the apply produces unexpected merge results.
|
|
38
|
-
* startup runs.
|
|
100
|
+
* if the apply produces unexpected merge results.
|
|
39
101
|
*
|
|
40
102
|
* Failures are best-effort: a list error (no repo, git unavailable) returns
|
|
41
103
|
* an empty result. An apply error becomes a warning the user sees alongside
|
|
@@ -45,12 +107,7 @@ export function auditOrphanedPreflightStashes(basePath, isMilestoneComplete) {
|
|
|
45
107
|
const result = { applied: [], warnings: [] };
|
|
46
108
|
let listOutput;
|
|
47
109
|
try {
|
|
48
|
-
listOutput =
|
|
49
|
-
cwd: basePath,
|
|
50
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
-
encoding: "utf-8",
|
|
52
|
-
env: GIT_NO_PROMPT_ENV,
|
|
53
|
-
});
|
|
110
|
+
listOutput = gitOutput(basePath, ["stash", "list", "--format=%gd%x00%s"]);
|
|
54
111
|
}
|
|
55
112
|
catch {
|
|
56
113
|
return result;
|
|
@@ -78,13 +135,32 @@ export function auditOrphanedPreflightStashes(basePath, isMilestoneComplete) {
|
|
|
78
135
|
}
|
|
79
136
|
if (!complete)
|
|
80
137
|
continue;
|
|
138
|
+
const statusEntries = readWorkingTreeStatus(basePath);
|
|
139
|
+
if (!statusEntries) {
|
|
140
|
+
result.warnings.push(manualApplyWarning(ref, milestoneId, "could not verify working tree status"));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (statusEntries.length > 0) {
|
|
144
|
+
if (isAlreadyRestoredUntrackedStatus(basePath, ref, statusEntries))
|
|
145
|
+
continue;
|
|
146
|
+
result.warnings.push(manualApplyWarning(ref, milestoneId, "working tree not clean"));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const trackedPaths = listStashTrackedPaths(basePath, ref);
|
|
150
|
+
if (trackedPaths === null) {
|
|
151
|
+
result.warnings.push(manualApplyWarning(ref, milestoneId, "could not inspect tracked paths in stash"));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (trackedPaths.length > 0) {
|
|
155
|
+
// A retained tracked-file stash has no durable marker showing whether a
|
|
156
|
+
// clean tree needs first recovery or would be re-mutated by an old backup.
|
|
157
|
+
const preview = trackedPaths.slice(0, 5).join(", ");
|
|
158
|
+
const suffix = trackedPaths.length > 5 ? `, and ${trackedPaths.length - 5} more` : "";
|
|
159
|
+
result.warnings.push(manualApplyWarning(ref, milestoneId, `stash modifies tracked files (${preview}${suffix})`));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
81
162
|
try {
|
|
82
|
-
|
|
83
|
-
cwd: basePath,
|
|
84
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
85
|
-
encoding: "utf-8",
|
|
86
|
-
env: GIT_NO_PROMPT_ENV,
|
|
87
|
-
});
|
|
163
|
+
gitOutput(basePath, ["stash", "apply", "--quiet", ref]);
|
|
88
164
|
result.applied.push({ milestoneId, stashRef: ref });
|
|
89
165
|
}
|
|
90
166
|
catch (err) {
|
|
@@ -45,7 +45,7 @@ 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 `gsd_plan_slice.tasks[]` shape: `taskId`, `title`, `description`, `estimate`, `files`, `verify`, `inputs`, `expectedOutput`, and optional `observabilityImpact`. `description` should contain the Why / Do / Done-when narrative. `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.
|
|
48
|
+
9. Each task needs the exact `gsd_plan_slice.tasks[]` shape: `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
49
|
10. Persist with `gsd_plan_slice` using `milestoneId`, `sliceId`, `goal`, optional `successCriteria`/`proofLevel`/`integrationClosure`/`observabilityImpact`, and `tasks`. `gsd_plan_slice` handles task persistence transactionally and renders `{{outputPath}}` plus task plans; do not call `gsd_plan_task`. The DB-backed tool is 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`.
|
|
@@ -44,8 +44,11 @@ export function validateFileChanges(basePath, expectedOutput, plannedFiles, file
|
|
|
44
44
|
const actualFiles = getChangedFilesFromLastCommit(basePath);
|
|
45
45
|
if (!actualFiles)
|
|
46
46
|
return null;
|
|
47
|
-
// Filter out
|
|
48
|
-
const projectFiles = actualFiles.filter(f => !f.startsWith(".gsd/") &&
|
|
47
|
+
// Filter out GSD-internal paths — only validate project source files.
|
|
48
|
+
const projectFiles = actualFiles.filter((f) => !f.startsWith(".gsd/") &&
|
|
49
|
+
!f.startsWith(".gsd\\") &&
|
|
50
|
+
!f.startsWith(".gsd-backups/") &&
|
|
51
|
+
!f.startsWith(".gsd-backups\\"));
|
|
49
52
|
// Normalize expected paths (strip leading ./ or /)
|
|
50
53
|
const normalizedExpected = new Set([...allExpected].map((f) => normalizePlannedFileReference(f).replace(/^\.\//, "").replace(/^\//, "")));
|
|
51
54
|
// Build allowlist matchers once (dot: true so patterns like `**/.hidden` work).
|
|
@@ -13,7 +13,7 @@ import { existsSync } from "node:fs";
|
|
|
13
13
|
import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, getLatestAssessmentByScope, updateMilestoneStatus, } from "../gsd-db.js";
|
|
14
14
|
import { clearPathCache, relMilestoneFile } from "../paths.js";
|
|
15
15
|
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
16
|
-
import { isClosedStatus } from "../status-guards.js";
|
|
16
|
+
import { isClosedStatus, isDeferredStatus } from "../status-guards.js";
|
|
17
17
|
import { saveFile, clearParseCache } from "../files.js";
|
|
18
18
|
import { invalidateStateCache } from "../state.js";
|
|
19
19
|
import { stripIdPrefix } from "../workflow-projections.js";
|
|
@@ -118,7 +118,7 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
118
118
|
guardError = `no slices found for milestone ${params.milestoneId}`;
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
-
const incompleteSlices = slices.filter(s => !isClosedStatus(s.status));
|
|
121
|
+
const incompleteSlices = slices.filter(s => !isClosedStatus(s.status) && !isDeferredStatus(s.status));
|
|
122
122
|
if (incompleteSlices.length > 0) {
|
|
123
123
|
const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
|
|
124
124
|
guardError = `incomplete slices: ${incompleteIds}`;
|
|
@@ -126,6 +126,8 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
126
126
|
}
|
|
127
127
|
// Deep check: verify all tasks in all slices are complete
|
|
128
128
|
for (const slice of slices) {
|
|
129
|
+
if (isDeferredStatus(slice.status))
|
|
130
|
+
continue;
|
|
129
131
|
const tasks = getSliceTasks(params.milestoneId, slice.id);
|
|
130
132
|
const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
|
|
131
133
|
if (incompleteTasks.length > 0) {
|
|
@@ -13,6 +13,7 @@ import { gsdRoot, resolveTasksDir, resolveSlicePath, resolveSliceFile, resolveTa
|
|
|
13
13
|
import { sendDesktopNotification } from "./notifications.js";
|
|
14
14
|
import { getTask, getSlice, getSliceTasks, updateTaskStatus, resetSliceCascade } from "./gsd-db.js";
|
|
15
15
|
import { renderPlanCheckboxes, renderRoadmapCheckboxes } from "./markdown-renderer.js";
|
|
16
|
+
import { UNIT_REGISTRY } from "./unit-registry.js";
|
|
16
17
|
/**
|
|
17
18
|
* Undo the last completed unit: revert git commits,
|
|
18
19
|
* delete summary artifacts, and uncheck the task in PLAN.
|
|
@@ -35,15 +36,19 @@ export async function handleUndo(args, ctx, _pi, basePath) {
|
|
|
35
36
|
ctx.ui.notify("Nothing to undo — no activity logs found.", "info");
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
// Extract unit type and ID from the most recent activity log filename
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// Extract unit type and ID from the most recent activity log filename.
|
|
40
|
+
// Both the unit type and the unit ID may contain hyphens, so anchor on the
|
|
41
|
+
// known unit-type vocabulary instead of guessing the unit-ID shape: a regex
|
|
42
|
+
// tuned to milestone-shaped IDs rejects project-level units whose IDs are
|
|
43
|
+
// symbolic (e.g. discuss-project uses PROJECT, workflow-preferences uses
|
|
44
|
+
// WORKFLOW-PREFS).
|
|
45
|
+
const parsed = parseActivityLogFilename(files[0]);
|
|
46
|
+
if (!parsed) {
|
|
42
47
|
ctx.ui.notify("Nothing to undo — could not parse latest activity log.", "warning");
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
45
|
-
const unitType =
|
|
46
|
-
const unitId =
|
|
50
|
+
const unitType = parsed.unitType;
|
|
51
|
+
const unitId = parsed.unitId.replace(/-/g, "/");
|
|
47
52
|
if (!force) {
|
|
48
53
|
ctx.ui.notify(`Will undo: ${unitType} (${unitId})\n` +
|
|
49
54
|
`This will:\n` +
|
|
@@ -87,6 +92,11 @@ export async function handleUndo(args, ctx, _pi, basePath) {
|
|
|
87
92
|
if (unitType === "execute-task" && task !== undefined && slice !== undefined) {
|
|
88
93
|
const [mid, sid, tid] = [milestone, slice, task];
|
|
89
94
|
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
|
|
95
|
+
if (getTask(mid, sid, tid)) {
|
|
96
|
+
updateTaskStatus(mid, sid, tid, "pending");
|
|
97
|
+
await renderPlanCheckboxes(basePath, mid, sid);
|
|
98
|
+
planUpdated = true;
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
// 3. Try to revert git commits from activity log
|
|
92
102
|
let commitsReverted = 0;
|
|
@@ -318,6 +328,33 @@ export async function handleResetSlice(args, ctx, _pi, basePath) {
|
|
|
318
328
|
ctx.ui.notify(results.join("\n"), "success");
|
|
319
329
|
}
|
|
320
330
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
331
|
+
// Known unit types sorted longest-first so a more specific type (e.g.
|
|
332
|
+
// "execute-task-simple") matches before a prefix of it ("execute-task") when
|
|
333
|
+
// splitting "<seq>-<unitType>-<unitId>.jsonl".
|
|
334
|
+
const UNIT_TYPES_BY_LENGTH_DESC = Object.keys(UNIT_REGISTRY).sort((a, b) => b.length - a.length);
|
|
335
|
+
/**
|
|
336
|
+
* Parse an activity-log filename of the form `<seq>-<unitType>-<unitId>.jsonl`
|
|
337
|
+
* (the format written by activity-log.ts). Both the unit type and the unit ID
|
|
338
|
+
* may contain hyphens, so we anchor on the known unit-type vocabulary rather
|
|
339
|
+
* than guessing the ID shape. This keeps non-milestone IDs (e.g. PROJECT,
|
|
340
|
+
* WORKFLOW-PREFS) parseable. Returns null when the name has no sequence prefix
|
|
341
|
+
* or does not start with a recognised unit type.
|
|
342
|
+
*/
|
|
343
|
+
export function parseActivityLogFilename(filename) {
|
|
344
|
+
const seqMatch = filename.match(/^\d+-(.+)\.jsonl$/);
|
|
345
|
+
if (!seqMatch)
|
|
346
|
+
return null;
|
|
347
|
+
const rest = seqMatch[1];
|
|
348
|
+
for (const unitType of UNIT_TYPES_BY_LENGTH_DESC) {
|
|
349
|
+
const prefix = `${unitType}-`;
|
|
350
|
+
if (rest.startsWith(prefix)) {
|
|
351
|
+
const unitId = rest.slice(prefix.length);
|
|
352
|
+
if (unitId.length > 0)
|
|
353
|
+
return { unitType, unitId };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
321
358
|
export function uncheckTaskInPlan(basePath, mid, sid, tid) {
|
|
322
359
|
const slicePath = resolveSlicePath(basePath, mid, sid);
|
|
323
360
|
if (!slicePath)
|