gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.bbb2f88ce
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- 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 +14 -14
- 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/required-server-files.json +1 -1
- 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 +14 -14
- 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/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +6 -1
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → 3HYkAopiKls15zp5a8I9n}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → 3HYkAopiKls15zp5a8I9n}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
b873eb2a8c4529c9
|
|
@@ -1089,7 +1089,12 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1089
1089
|
s.lastGitActionFailure = null;
|
|
1090
1090
|
s.lastGitActionStatus = null;
|
|
1091
1091
|
s.lastUnitAgentEndMessages = null;
|
|
1092
|
-
setCurrentPhase(unitType
|
|
1092
|
+
setCurrentPhase(unitType, {
|
|
1093
|
+
basePath: s.basePath,
|
|
1094
|
+
traceId: ic.flowId,
|
|
1095
|
+
turnId: `iter-${ic.iteration}`,
|
|
1096
|
+
causedBy: "unit-start",
|
|
1097
|
+
});
|
|
1093
1098
|
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1094
1099
|
const unitStartSeq = ic.nextSeq();
|
|
1095
1100
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
@@ -128,9 +128,9 @@ export function diagnoseExpectedArtifact(unitType, unitId, base) {
|
|
|
128
128
|
}
|
|
129
129
|
return `${relSliceFile(base, mid, sid, "RESEARCH")} (slice research)`;
|
|
130
130
|
case "plan-slice":
|
|
131
|
-
return `${relSliceFile(base, mid, sid, "PLAN")} (slice plan)`;
|
|
131
|
+
return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (slice plan and task plans)`;
|
|
132
132
|
case "refine-slice":
|
|
133
|
-
return `${relSliceFile(base, mid, sid, "PLAN")} (refined slice plan
|
|
133
|
+
return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (refined slice plan and task plans)`;
|
|
134
134
|
case "execute-task": {
|
|
135
135
|
return `Task ${tid} marked [x] in ${relSliceFile(base, mid, sid, "PLAN")} + summary written`;
|
|
136
136
|
}
|
|
@@ -559,6 +559,8 @@ export const DISPATCH_RULES = [
|
|
|
559
559
|
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
560
560
|
if (hasContext)
|
|
561
561
|
return null; // fall through to next rule
|
|
562
|
+
if (prefs?.planning_depth === "deep")
|
|
563
|
+
return null;
|
|
562
564
|
// H6 fix (#4973): keep the non-deep auto-mode bypass, but do not
|
|
563
565
|
// pre-verify deep planning's user-facing milestone approval gate.
|
|
564
566
|
if (shouldBypassMilestoneDepthGateInAuto(prefs)) {
|
|
@@ -257,7 +257,7 @@ function scanGsdTaggedCommits(basePath, milestoneId, gitArgs) {
|
|
|
257
257
|
if (!commitMessageHasGsdTrailer(message))
|
|
258
258
|
continue;
|
|
259
259
|
const commitFiles = getChangedFilesForCommit(basePath, hash);
|
|
260
|
-
if (!commitMatchesMilestone(message, milestoneId, commitFiles))
|
|
260
|
+
if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles))
|
|
261
261
|
continue;
|
|
262
262
|
matched = true;
|
|
263
263
|
for (const file of commitFiles) {
|
|
@@ -278,22 +278,37 @@ function getChangedFilesForCommit(basePath, hash) {
|
|
|
278
278
|
function commitMessageHasGsdTrailer(message) {
|
|
279
279
|
return /^GSD-(?:Task|Unit):\s*\S+/m.test(message);
|
|
280
280
|
}
|
|
281
|
-
function commitMatchesMilestone(message, milestoneId, files) {
|
|
281
|
+
function commitMatchesMilestone(basePath, message, milestoneId, files) {
|
|
282
282
|
if (commitTrailerStartsWithMilestone(message, milestoneId))
|
|
283
283
|
return true;
|
|
284
284
|
// Meaningful execute-task commits currently store task scope as Sxx/Tyy
|
|
285
285
|
// rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
|
|
286
286
|
// either the commit touched this milestone's artifacts, or — for projects
|
|
287
287
|
// where .gsd/ is gitignored/external (#5033) — the message explicitly
|
|
288
|
-
// names the milestone.
|
|
288
|
+
// names the milestone or local GSD state proves the task belongs here.
|
|
289
289
|
if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
|
|
290
290
|
if (files.some((file) => isMilestoneArtifactPath(file, milestoneId)))
|
|
291
291
|
return true;
|
|
292
292
|
if (commitMessageMentionsMilestone(message, milestoneId))
|
|
293
293
|
return true;
|
|
294
|
+
if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId))
|
|
295
|
+
return true;
|
|
294
296
|
}
|
|
295
297
|
return false;
|
|
296
298
|
}
|
|
299
|
+
function commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId) {
|
|
300
|
+
const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
|
|
301
|
+
if (!match)
|
|
302
|
+
return false;
|
|
303
|
+
const [, sliceId, taskId] = match;
|
|
304
|
+
if (getTask(milestoneId, sliceId, taskId))
|
|
305
|
+
return true;
|
|
306
|
+
const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
307
|
+
if (!tasksDir)
|
|
308
|
+
return false;
|
|
309
|
+
return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
|
|
310
|
+
|| existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
|
|
311
|
+
}
|
|
297
312
|
function commitMessageMentionsMilestone(message, milestoneId) {
|
|
298
313
|
if (!MILESTONE_ID_RE.test(milestoneId))
|
|
299
314
|
return false;
|
|
@@ -486,8 +486,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
486
486
|
// Clear survivor flag — finalization is done
|
|
487
487
|
hasSurvivorBranch = false;
|
|
488
488
|
}
|
|
489
|
+
const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
|
|
489
490
|
const deepProjectStagePending = !hasSurvivorBranch
|
|
490
|
-
? (await import("./auto-dispatch.js")).hasPendingDeepStage(
|
|
491
|
+
? (await import("./auto-dispatch.js")).hasPendingDeepStage(effectivePrefs, base)
|
|
491
492
|
: false;
|
|
492
493
|
if (deepProjectStagePending) {
|
|
493
494
|
// Deep project-level setup runs before the first milestone exists. Let
|
|
@@ -526,7 +527,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
526
527
|
const mid = state.activeMilestone.id;
|
|
527
528
|
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
528
529
|
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
529
|
-
if (!hasContext) {
|
|
530
|
+
if (!hasContext && effectivePrefs?.planning_depth !== "deep") {
|
|
530
531
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
531
532
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
532
533
|
// showSmartEntry dispatches via pi.sendMessage() which is fire-and-forget:
|
|
@@ -177,8 +177,12 @@ export function resolveDir(parentDir, idPrefix) {
|
|
|
177
177
|
const exact = entries.find(e => e.isDirectory() && e.name === idPrefix);
|
|
178
178
|
if (exact)
|
|
179
179
|
return exact.name;
|
|
180
|
+
const idLower = idPrefix.toLowerCase();
|
|
181
|
+
const exactCaseInsensitive = entries.find(e => e.isDirectory() && e.name.toLowerCase() === idLower);
|
|
182
|
+
if (exactCaseInsensitive)
|
|
183
|
+
return exactCaseInsensitive.name;
|
|
180
184
|
// Prefix match for legacy descriptor dirs: M001-SOMETHING
|
|
181
|
-
const prefixed = entries.find(e => e.isDirectory() && e.name.startsWith(
|
|
185
|
+
const prefixed = entries.find(e => e.isDirectory() && e.name.toLowerCase().startsWith(idLower + "-"));
|
|
182
186
|
return prefixed ? prefixed.name : null;
|
|
183
187
|
}
|
|
184
188
|
catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 UOK Audit Events and DB-First Projection Writes
|
|
1
2
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -5,6 +6,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
|
5
6
|
import { withFileLockSync } from "../file-lock.js";
|
|
6
7
|
import { gsdRoot } from "../paths.js";
|
|
7
8
|
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
|
|
9
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent } from "./contracts.js";
|
|
8
10
|
function auditLogPath(basePath) {
|
|
9
11
|
return join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
10
12
|
}
|
|
@@ -13,6 +15,7 @@ function ensureAuditDir(basePath) {
|
|
|
13
15
|
}
|
|
14
16
|
export function buildAuditEnvelope(args) {
|
|
15
17
|
return {
|
|
18
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
16
19
|
eventId: randomUUID(),
|
|
17
20
|
traceId: args.traceId,
|
|
18
21
|
turnId: args.turnId,
|
|
@@ -27,6 +30,25 @@ export function emitUokAuditEvent(basePath, event) {
|
|
|
27
30
|
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
28
31
|
if (isStaleWrite("uok-audit"))
|
|
29
32
|
return;
|
|
33
|
+
const validation = validateAuditEvent(event);
|
|
34
|
+
if (!validation.ok) {
|
|
35
|
+
throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
36
|
+
}
|
|
37
|
+
const canonical = validation.value;
|
|
38
|
+
if (isDbAvailable()) {
|
|
39
|
+
try {
|
|
40
|
+
insertAuditEvent({
|
|
41
|
+
...canonical,
|
|
42
|
+
payload: {
|
|
43
|
+
...canonical.payload,
|
|
44
|
+
contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
30
52
|
try {
|
|
31
53
|
ensureAuditDir(basePath);
|
|
32
54
|
const path = auditLogPath(basePath);
|
|
@@ -39,18 +61,10 @@ export function emitUokAuditEvent(basePath, event) {
|
|
|
39
61
|
// POSIX O_APPEND atomicity still protects small line writes, so skipping
|
|
40
62
|
// the lock rather than stalling orchestration is the correct tradeoff.
|
|
41
63
|
withFileLockSync(path, () => {
|
|
42
|
-
appendFileSync(path, `${JSON.stringify(
|
|
64
|
+
appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
|
|
43
65
|
}, { onLocked: "skip" });
|
|
44
66
|
}
|
|
45
67
|
catch {
|
|
46
68
|
// Best-effort: audit writes must never break orchestration.
|
|
47
69
|
}
|
|
48
|
-
if (!isDbAvailable())
|
|
49
|
-
return;
|
|
50
|
-
try {
|
|
51
|
-
insertAuditEvent(event);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Projection failures are non-fatal while legacy readers are still active.
|
|
55
|
-
}
|
|
56
70
|
}
|
|
@@ -1 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
// GSD2 UOK Contract Types and Versioning
|
|
2
|
+
export const CURRENT_UOK_CONTRACT_VERSION = "1";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function normalizeVersion(value) {
|
|
7
|
+
return value === CURRENT_UOK_CONTRACT_VERSION ? CURRENT_UOK_CONTRACT_VERSION : "0";
|
|
8
|
+
}
|
|
9
|
+
function requireString(value, key, issues) {
|
|
10
|
+
if (typeof value[key] !== "string" || value[key] === "") {
|
|
11
|
+
issues.push({ path: key, message: `${key} must be a non-empty string` });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function requireRecord(value, key, issues) {
|
|
15
|
+
if (!isRecord(value[key])) {
|
|
16
|
+
issues.push({ path: key, message: `${key} must be an object` });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function normalizeTurnResult(value) {
|
|
20
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
21
|
+
}
|
|
22
|
+
export function normalizeDispatchEnvelope(value) {
|
|
23
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
24
|
+
}
|
|
25
|
+
export function normalizeAuditEvent(value) {
|
|
26
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
27
|
+
}
|
|
28
|
+
export function validateTurnResult(value) {
|
|
29
|
+
const normalized = normalizeTurnResult(value);
|
|
30
|
+
const record = normalized;
|
|
31
|
+
const issues = [];
|
|
32
|
+
requireString(record, "traceId", issues);
|
|
33
|
+
requireString(record, "turnId", issues);
|
|
34
|
+
if (!Number.isInteger(record.iteration)) {
|
|
35
|
+
issues.push({ path: "iteration", message: "iteration must be an integer" });
|
|
36
|
+
}
|
|
37
|
+
requireString(record, "status", issues);
|
|
38
|
+
requireString(record, "failureClass", issues);
|
|
39
|
+
if (!Array.isArray(record.phaseResults)) {
|
|
40
|
+
issues.push({ path: "phaseResults", message: "phaseResults must be an array" });
|
|
41
|
+
}
|
|
42
|
+
requireString(record, "startedAt", issues);
|
|
43
|
+
requireString(record, "finishedAt", issues);
|
|
44
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
45
|
+
}
|
|
46
|
+
export function validateDispatchEnvelope(value) {
|
|
47
|
+
const normalized = normalizeDispatchEnvelope(value);
|
|
48
|
+
const record = normalized;
|
|
49
|
+
const issues = [];
|
|
50
|
+
requireString(record, "action", issues);
|
|
51
|
+
requireRecord(record, "reason", issues);
|
|
52
|
+
if (isRecord(record.reason)) {
|
|
53
|
+
requireString(record.reason, "reasonCode", issues);
|
|
54
|
+
requireString(record.reason, "summary", issues);
|
|
55
|
+
}
|
|
56
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
57
|
+
}
|
|
58
|
+
export function validateAuditEvent(value) {
|
|
59
|
+
const normalized = normalizeAuditEvent(value);
|
|
60
|
+
const record = normalized;
|
|
61
|
+
const issues = [];
|
|
62
|
+
requireString(record, "eventId", issues);
|
|
63
|
+
requireString(record, "traceId", issues);
|
|
64
|
+
requireString(record, "category", issues);
|
|
65
|
+
requireString(record, "type", issues);
|
|
66
|
+
requireString(record, "ts", issues);
|
|
67
|
+
requireRecord(record, "payload", issues);
|
|
68
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
69
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// GSD2 UOK Dispatch Envelope Builder
|
|
2
|
+
import { CURRENT_UOK_CONTRACT_VERSION } from "./contracts.js";
|
|
1
3
|
export function buildDispatchEnvelope(input) {
|
|
2
4
|
return {
|
|
5
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
3
6
|
action: input.action,
|
|
4
7
|
nodeKind: input.node?.kind,
|
|
5
8
|
unitType: input.unitType,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD2 UOK Turn Observer and DB-Backed Lifecycle Emission
|
|
2
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateTurnResult } from "./contracts.js";
|
|
1
3
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
|
|
2
4
|
import { writeTurnCloseoutGitRecord, writeTurnGitTransaction } from "./gitops.js";
|
|
3
5
|
import { acquireWriterToken, nextWriteRecord, releaseWriterToken } from "./writer.js";
|
|
@@ -115,46 +117,59 @@ export function createTurnObserver(options) {
|
|
|
115
117
|
}
|
|
116
118
|
},
|
|
117
119
|
onTurnResult(result) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
if (writerToken) {
|
|
122
|
+
releaseWriterToken(options.basePath, writerToken);
|
|
123
|
+
}
|
|
124
|
+
writerToken = null;
|
|
125
|
+
current = null;
|
|
126
|
+
phaseResults.length = 0;
|
|
121
127
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
try {
|
|
129
|
+
const merged = {
|
|
130
|
+
...result,
|
|
131
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
132
|
+
phaseResults: Array.isArray(result.phaseResults) && result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
|
|
133
|
+
};
|
|
134
|
+
const validation = validateTurnResult(merged);
|
|
135
|
+
if (!validation.ok) {
|
|
136
|
+
throw new Error(`Invalid UOK turn result: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
137
|
+
}
|
|
138
|
+
if (options.enableAudit) {
|
|
139
|
+
emitUokAuditEvent(options.basePath, buildAuditEnvelope({
|
|
140
|
+
traceId: validation.value.traceId,
|
|
141
|
+
turnId: validation.value.turnId,
|
|
142
|
+
category: "orchestration",
|
|
143
|
+
type: "turn-result",
|
|
144
|
+
payload: nextSequenceMetadata("audit", "append", {
|
|
145
|
+
contractVersion: validation.value.version,
|
|
146
|
+
unitType: validation.value.unitType,
|
|
147
|
+
unitId: validation.value.unitId,
|
|
148
|
+
status: validation.value.status,
|
|
149
|
+
failureClass: validation.value.failureClass,
|
|
150
|
+
error: validation.value.error,
|
|
151
|
+
phaseCount: validation.value.phaseResults.length,
|
|
152
|
+
}),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
if (options.enableGitops) {
|
|
156
|
+
const closeout = merged.closeout ?? {
|
|
157
|
+
traceId: merged.traceId,
|
|
158
|
+
turnId: merged.turnId,
|
|
129
159
|
unitType: merged.unitType,
|
|
130
160
|
unitId: merged.unitId,
|
|
131
161
|
status: merged.status,
|
|
132
162
|
failureClass: merged.failureClass,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
gitAction: options.gitAction,
|
|
164
|
+
gitPushed: options.gitPush,
|
|
165
|
+
finishedAt: merged.finishedAt,
|
|
166
|
+
};
|
|
167
|
+
writeTurnCloseoutGitRecord(options.basePath, closeout, nextSequenceMetadata("gitops", "update", { action: "record" }));
|
|
168
|
+
}
|
|
137
169
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
traceId: merged.traceId,
|
|
141
|
-
turnId: merged.turnId,
|
|
142
|
-
unitType: merged.unitType,
|
|
143
|
-
unitId: merged.unitId,
|
|
144
|
-
status: merged.status,
|
|
145
|
-
failureClass: merged.failureClass,
|
|
146
|
-
gitAction: options.gitAction,
|
|
147
|
-
gitPushed: options.gitPush,
|
|
148
|
-
finishedAt: merged.finishedAt,
|
|
149
|
-
};
|
|
150
|
-
writeTurnCloseoutGitRecord(options.basePath, closeout, nextSequenceMetadata("gitops", "update", { action: "record" }));
|
|
170
|
+
finally {
|
|
171
|
+
cleanup();
|
|
151
172
|
}
|
|
152
|
-
if (writerToken) {
|
|
153
|
-
releaseWriterToken(options.basePath, writerToken);
|
|
154
|
-
}
|
|
155
|
-
writerToken = null;
|
|
156
|
-
current = null;
|
|
157
|
-
phaseResults.length = 0;
|
|
158
173
|
},
|
|
159
174
|
};
|
|
160
175
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// GSD2 UOK Timeline Reconstruction from Authoritative DB Records
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { _getAdapter, isDbAvailable } from "../gsd-db.js";
|
|
5
|
+
import { gsdRoot } from "../paths.js";
|
|
6
|
+
function parseJsonRecord(value) {
|
|
7
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
11
|
+
return {};
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(value);
|
|
14
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
15
|
+
? parsed
|
|
16
|
+
: {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function matchesFilter(entry, filter) {
|
|
23
|
+
if (filter.traceId && entry.traceId !== filter.traceId)
|
|
24
|
+
return false;
|
|
25
|
+
if (filter.turnId && entry.turnId !== filter.turnId)
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
function byTimestamp(a, b) {
|
|
30
|
+
return a.ts.localeCompare(b.ts);
|
|
31
|
+
}
|
|
32
|
+
function readDbTimeline(filter) {
|
|
33
|
+
const db = _getAdapter();
|
|
34
|
+
if (!db)
|
|
35
|
+
return [];
|
|
36
|
+
const entries = [];
|
|
37
|
+
const where = [];
|
|
38
|
+
const params = {};
|
|
39
|
+
if (filter.traceId) {
|
|
40
|
+
where.push("trace_id = :trace_id");
|
|
41
|
+
params[":trace_id"] = filter.traceId;
|
|
42
|
+
}
|
|
43
|
+
if (filter.turnId) {
|
|
44
|
+
where.push("turn_id = :turn_id");
|
|
45
|
+
params[":turn_id"] = filter.turnId;
|
|
46
|
+
}
|
|
47
|
+
const suffix = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
|
|
48
|
+
const auditRows = db.prepare(`SELECT trace_id, turn_id, type, ts, payload_json FROM audit_events${suffix}`).all(params);
|
|
49
|
+
for (const row of auditRows) {
|
|
50
|
+
entries.push({
|
|
51
|
+
source: "audit_events",
|
|
52
|
+
ts: row.ts,
|
|
53
|
+
traceId: row.trace_id,
|
|
54
|
+
turnId: row.turn_id,
|
|
55
|
+
type: row.type,
|
|
56
|
+
payload: parseJsonRecord(row.payload_json),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const dispatchRows = db.prepare(`SELECT trace_id, turn_id, unit_type, unit_id, status, started_at, ended_at, exit_reason,
|
|
60
|
+
error_summary, retry_after_ms, attempt_n, max_attempts
|
|
61
|
+
FROM unit_dispatches${suffix}`).all(params);
|
|
62
|
+
for (const row of dispatchRows) {
|
|
63
|
+
entries.push({
|
|
64
|
+
source: "unit_dispatches",
|
|
65
|
+
ts: String(row.ended_at ?? row.started_at ?? ""),
|
|
66
|
+
traceId: String(row.trace_id ?? ""),
|
|
67
|
+
turnId: typeof row.turn_id === "string" ? row.turn_id : null,
|
|
68
|
+
type: `dispatch-${String(row.status ?? "unknown")}`,
|
|
69
|
+
payload: { ...row },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const gitRows = db.prepare(`SELECT trace_id, turn_id, unit_type, unit_id, stage, action, push, status, error,
|
|
73
|
+
metadata_json, updated_at
|
|
74
|
+
FROM turn_git_transactions${suffix}`).all(params);
|
|
75
|
+
for (const row of gitRows) {
|
|
76
|
+
entries.push({
|
|
77
|
+
source: "turn_git_transactions",
|
|
78
|
+
ts: String(row.updated_at ?? ""),
|
|
79
|
+
traceId: String(row.trace_id ?? ""),
|
|
80
|
+
turnId: typeof row.turn_id === "string" ? row.turn_id : null,
|
|
81
|
+
type: `gitops-${String(row.stage ?? "unknown")}`,
|
|
82
|
+
payload: {
|
|
83
|
+
...row,
|
|
84
|
+
metadata: parseJsonRecord(row.metadata_json),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return entries.filter((entry) => entry.ts !== "").sort(byTimestamp);
|
|
89
|
+
}
|
|
90
|
+
function readJsonlTimeline(basePath, filter) {
|
|
91
|
+
const path = join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
92
|
+
if (!existsSync(path))
|
|
93
|
+
return [];
|
|
94
|
+
return readFileSync(path, "utf-8")
|
|
95
|
+
.split("\n")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((line) => {
|
|
98
|
+
const event = parseJsonRecord(line);
|
|
99
|
+
const entry = {
|
|
100
|
+
source: "audit_jsonl",
|
|
101
|
+
ts: String(event.ts ?? ""),
|
|
102
|
+
traceId: typeof event.traceId === "string" ? event.traceId : undefined,
|
|
103
|
+
turnId: typeof event.turnId === "string" ? event.turnId : null,
|
|
104
|
+
type: String(event.type ?? "audit"),
|
|
105
|
+
payload: parseJsonRecord(event.payload),
|
|
106
|
+
};
|
|
107
|
+
return entry.ts && matchesFilter(entry, filter) ? entry : null;
|
|
108
|
+
})
|
|
109
|
+
.filter((entry) => entry !== null)
|
|
110
|
+
.sort(byTimestamp);
|
|
111
|
+
}
|
|
112
|
+
export function buildTurnTimeline(basePath, filter = {}) {
|
|
113
|
+
if (isDbAvailable()) {
|
|
114
|
+
return {
|
|
115
|
+
authoritative: "db",
|
|
116
|
+
degraded: false,
|
|
117
|
+
entries: readDbTimeline(filter),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
authoritative: "degraded-fallback",
|
|
122
|
+
degraded: true,
|
|
123
|
+
entries: readJsonlTimeline(basePath, filter),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -1,29 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GSD2 Phase State — cross-extension coordination
|
|
3
3
|
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
4
4
|
*
|
|
5
5
|
* Lightweight module-level state that GSD auto-mode writes to and the
|
|
6
6
|
* subagent tool reads from. Both extensions run in the same process so
|
|
7
7
|
* a module variable is sufficient — no file I/O needed.
|
|
8
8
|
*/
|
|
9
|
+
import { buildAuditEnvelope, emitUokAuditEvent } from "../gsd/uok/audit.js";
|
|
9
10
|
let _active = false;
|
|
10
11
|
let _currentPhase = null;
|
|
12
|
+
let _auditContext = null;
|
|
13
|
+
function emitPhaseChange(action, previousPhase, nextPhase) {
|
|
14
|
+
if (!_auditContext)
|
|
15
|
+
return;
|
|
16
|
+
emitUokAuditEvent(_auditContext.basePath, buildAuditEnvelope({
|
|
17
|
+
traceId: _auditContext.traceId,
|
|
18
|
+
turnId: _auditContext.turnId,
|
|
19
|
+
causedBy: _auditContext.causedBy,
|
|
20
|
+
category: "orchestration",
|
|
21
|
+
type: "phase_changed",
|
|
22
|
+
payload: {
|
|
23
|
+
action,
|
|
24
|
+
active: _active,
|
|
25
|
+
previousPhase,
|
|
26
|
+
nextPhase,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
export function configureGSDPhaseAudit(context) {
|
|
31
|
+
_auditContext = context;
|
|
32
|
+
}
|
|
11
33
|
/** Mark GSD auto-mode as active. */
|
|
12
|
-
export function activateGSD() {
|
|
34
|
+
export function activateGSD(context) {
|
|
35
|
+
if (context)
|
|
36
|
+
_auditContext = context;
|
|
37
|
+
const previousPhase = _currentPhase;
|
|
13
38
|
_active = true;
|
|
39
|
+
emitPhaseChange("activate", previousPhase, _currentPhase);
|
|
14
40
|
}
|
|
15
41
|
/** Mark GSD auto-mode as inactive and clear the current phase. */
|
|
16
42
|
export function deactivateGSD() {
|
|
43
|
+
const previousPhase = _currentPhase;
|
|
17
44
|
_active = false;
|
|
18
45
|
_currentPhase = null;
|
|
46
|
+
emitPhaseChange("deactivate", previousPhase, _currentPhase);
|
|
47
|
+
_auditContext = null;
|
|
19
48
|
}
|
|
20
49
|
/** Set the currently dispatched GSD phase (e.g. "plan-milestone"). */
|
|
21
|
-
export function setCurrentPhase(phase) {
|
|
50
|
+
export function setCurrentPhase(phase, context) {
|
|
51
|
+
if (context)
|
|
52
|
+
_auditContext = context;
|
|
53
|
+
if (!_active) {
|
|
54
|
+
process.emitWarning(`Ignoring GSD phase "${phase}" while GSD auto-mode is inactive`, {
|
|
55
|
+
code: "GSD_PHASE_INACTIVE",
|
|
56
|
+
});
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const previousPhase = _currentPhase;
|
|
22
60
|
_currentPhase = phase;
|
|
61
|
+
emitPhaseChange("set", previousPhase, _currentPhase);
|
|
62
|
+
return true;
|
|
23
63
|
}
|
|
24
64
|
/** Clear the current phase (unit completed or aborted). */
|
|
25
65
|
export function clearCurrentPhase() {
|
|
66
|
+
const previousPhase = _currentPhase;
|
|
26
67
|
_currentPhase = null;
|
|
68
|
+
emitPhaseChange("clear", previousPhase, _currentPhase);
|
|
27
69
|
}
|
|
28
70
|
/** Returns true if GSD auto-mode is currently active. */
|
|
29
71
|
export function isGSDActive() {
|