gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.487011134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +94 -47
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +6 -1
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  5. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  6. package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
  7. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  8. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
  10. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  11. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  12. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  13. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
  14. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  15. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  16. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  17. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  18. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  19. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  20. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  21. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  56. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  57. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
  58. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  59. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
  60. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  61. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
  62. package/package.json +1 -1
  63. package/packages/daemon/package.json +2 -2
  64. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  67. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  68. package/packages/mcp-server/package.json +2 -2
  69. package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
  70. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  71. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  72. package/packages/native/package.json +1 -1
  73. package/packages/pi-agent-core/package.json +1 -1
  74. package/packages/pi-ai/package.json +1 -1
  75. package/packages/pi-coding-agent/package.json +1 -1
  76. package/packages/pi-tui/package.json +1 -1
  77. package/packages/rpc-client/package.json +1 -1
  78. package/pkg/package.json +1 -1
  79. package/src/resources/extensions/gsd/auto/phases.ts +6 -1
  80. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  81. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  82. package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
  83. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  84. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  85. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
  86. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  87. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  88. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  89. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
  90. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  91. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  92. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  93. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  94. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  95. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  96. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  97. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  98. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  99. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  100. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  101. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  102. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  103. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  104. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +100 -0
  105. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  106. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
  107. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  108. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  109. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  110. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  111. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  112. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  113. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  114. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  115. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  116. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_buildManifest.js +0 -0
  117. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_ssgManifest.js +0 -0
@@ -90,6 +90,17 @@ export type MemoryPolicy = "none" | "critical-only" | "prompt-relevant";
90
90
  /** Preferences block policy. */
91
91
  export type PreferencesPolicy = "none" | "active-only" | "full";
92
92
 
93
+ /** Context Mode lane guidance policy for each auto-mode unit. */
94
+ export type ContextModePolicy =
95
+ | "none"
96
+ | "interview"
97
+ | "research"
98
+ | "planning"
99
+ | "execution"
100
+ | "verification"
101
+ | "orchestration"
102
+ | "docs";
103
+
93
104
  /**
94
105
  * Tool-access policy per unit type (#4934).
95
106
  *
@@ -220,6 +231,8 @@ export interface UnitContextManifest {
220
231
  readonly codebaseMap: boolean;
221
232
  /** Preferences block policy. */
222
233
  readonly preferences: PreferencesPolicy;
234
+ /** Context Mode guidance lane. */
235
+ readonly contextMode: ContextModePolicy;
223
236
  /**
224
237
  * Tool-access policy (#4934). Runtime enforcement covers path-scoped write
225
238
  * blocking, subagent denial, and bash allowlisting for active auto-mode
@@ -343,6 +356,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
343
356
  memory: "prompt-relevant",
344
357
  codebaseMap: true,
345
358
  preferences: "active-only",
359
+ contextMode: "research",
346
360
  tools: TOOLS_PLANNING,
347
361
  artifacts: {
348
362
  // Phase 3 migration (#4782): matches today's actual
@@ -359,6 +373,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
359
373
  memory: "prompt-relevant",
360
374
  codebaseMap: true,
361
375
  preferences: "active-only",
376
+ contextMode: "planning",
362
377
  tools: TOOLS_PLANNING,
363
378
  artifacts: {
364
379
  inline: ["project", "requirements", "decisions", "milestone-research", "templates"],
@@ -373,6 +388,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
373
388
  memory: "prompt-relevant",
374
389
  codebaseMap: true,
375
390
  preferences: "active-only",
391
+ contextMode: "interview",
376
392
  tools: TOOLS_PLANNING,
377
393
  artifacts: {
378
394
  inline: ["project", "requirements", "decisions", "milestone-context", "templates"],
@@ -387,6 +403,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
387
403
  memory: "prompt-relevant",
388
404
  codebaseMap: false,
389
405
  preferences: "active-only",
406
+ contextMode: "verification",
390
407
  // planning-dispatch: validation is a verification-fan-out unit. It reads
391
408
  // the milestone surface and dispatches reviewer/security/tester subagents
392
409
  // to report findings without touching user source. Mirrors
@@ -405,6 +422,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
405
422
  memory: "prompt-relevant",
406
423
  codebaseMap: false,
407
424
  preferences: "active-only",
425
+ contextMode: "verification",
408
426
  // planning-dispatch: completion is a high-leverage place to fan out to
409
427
  // reviewer / security / tester subagents. They read the diff and report
410
428
  // findings; they do not write user source. Write isolation to .gsd/ is
@@ -428,6 +446,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
428
446
  memory: "prompt-relevant",
429
447
  codebaseMap: true,
430
448
  preferences: "active-only",
449
+ contextMode: "research",
431
450
  tools: TOOLS_PLANNING,
432
451
  artifacts: {
433
452
  inline: ["roadmap", "milestone-research", "dependency-summaries", "templates"],
@@ -442,6 +461,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
442
461
  memory: "prompt-relevant",
443
462
  codebaseMap: true,
444
463
  preferences: "active-only",
464
+ contextMode: "planning",
445
465
  // planning-dispatch: allows subagent dispatch so the planner can fan out
446
466
  // to scout for codebase recon and to planner/decompose-style specialists
447
467
  // for sub-decomposition. Write-isolation to .gsd/ is preserved.
@@ -459,6 +479,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
459
479
  memory: "prompt-relevant",
460
480
  codebaseMap: true,
461
481
  preferences: "active-only",
482
+ contextMode: "planning",
462
483
  // See plan-slice — same rationale: dispatch to scout/planner-style
463
484
  // specialists during refinement is materially better than re-doing recon
464
485
  // inline.
@@ -476,6 +497,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
476
497
  memory: "prompt-relevant",
477
498
  codebaseMap: true,
478
499
  preferences: "active-only",
500
+ contextMode: "planning",
479
501
  tools: TOOLS_PLANNING,
480
502
  artifacts: {
481
503
  inline: ["slice-plan", "slice-research", "dependency-summaries", "prior-task-summaries", "templates"],
@@ -490,6 +512,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
490
512
  memory: "prompt-relevant",
491
513
  codebaseMap: false,
492
514
  preferences: "active-only",
515
+ contextMode: "verification",
493
516
  // See complete-milestone — same rationale: dispatch to reviewer / security /
494
517
  // tester subagents to fan out review work without bloating this unit's
495
518
  // context.
@@ -511,6 +534,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
511
534
  memory: "critical-only",
512
535
  codebaseMap: false,
513
536
  preferences: "none",
537
+ contextMode: "planning",
514
538
  tools: TOOLS_PLANNING,
515
539
  artifacts: {
516
540
  // Phase 2 pilot (#4782): manifest now matches today's actual
@@ -530,6 +554,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
530
554
  memory: "prompt-relevant",
531
555
  codebaseMap: true,
532
556
  preferences: "active-only",
557
+ contextMode: "execution",
533
558
  tools: TOOLS_ALL,
534
559
  artifacts: {
535
560
  inline: ["task-plan", "slice-plan", "prior-task-summaries", "templates"],
@@ -544,6 +569,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
544
569
  memory: "prompt-relevant",
545
570
  codebaseMap: true,
546
571
  preferences: "active-only",
572
+ contextMode: "execution",
547
573
  tools: TOOLS_ALL,
548
574
  artifacts: {
549
575
  inline: ["slice-plan", "prior-task-summaries", "templates"],
@@ -560,6 +586,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
560
586
  memory: "critical-only",
561
587
  codebaseMap: false,
562
588
  preferences: "active-only",
589
+ contextMode: "verification",
563
590
  tools: TOOLS_PLANNING,
564
591
  artifacts: {
565
592
  // Phase 3 migration (#4782): manifest matches today's actual
@@ -578,6 +605,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
578
605
  memory: "critical-only",
579
606
  codebaseMap: false,
580
607
  preferences: "active-only",
608
+ contextMode: "verification",
581
609
  tools: TOOLS_PLANNING,
582
610
  artifacts: {
583
611
  inline: ["slice-plan", "prior-task-summaries"],
@@ -592,6 +620,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
592
620
  memory: "prompt-relevant",
593
621
  codebaseMap: true,
594
622
  preferences: "active-only",
623
+ contextMode: "docs",
595
624
  tools: TOOLS_DOCS,
596
625
  artifacts: {
597
626
  inline: ["project", "requirements", "decisions", "templates"],
@@ -611,6 +640,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
611
640
  memory: "none",
612
641
  codebaseMap: false,
613
642
  preferences: "none",
643
+ contextMode: "none",
614
644
  tools: TOOLS_PLANNING,
615
645
  artifacts: {
616
646
  inline: [],
@@ -628,6 +658,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
628
658
  memory: "prompt-relevant",
629
659
  codebaseMap: true,
630
660
  preferences: "active-only",
661
+ contextMode: "interview",
631
662
  tools: TOOLS_PLANNING,
632
663
  artifacts: {
633
664
  inline: ["templates"],
@@ -644,6 +675,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
644
675
  memory: "prompt-relevant",
645
676
  codebaseMap: true,
646
677
  preferences: "active-only",
678
+ contextMode: "interview",
647
679
  tools: TOOLS_PLANNING,
648
680
  artifacts: {
649
681
  inline: ["project", "templates"],
@@ -660,6 +692,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
660
692
  memory: "none",
661
693
  codebaseMap: false,
662
694
  preferences: "none",
695
+ contextMode: "none",
663
696
  tools: TOOLS_PLANNING,
664
697
  artifacts: {
665
698
  inline: [],
@@ -678,6 +711,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
678
711
  memory: "prompt-relevant",
679
712
  codebaseMap: true,
680
713
  preferences: "active-only",
714
+ contextMode: "research",
681
715
  tools: { mode: "planning-dispatch", allowedSubagents: ["scout"] },
682
716
  artifacts: {
683
717
  inline: ["project", "requirements", "templates"],
@@ -1,3 +1,5 @@
1
+ // GSD2 UOK Audit Events and DB-First Projection Writes
2
+
1
3
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { randomUUID } from "node:crypto";
@@ -6,7 +8,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
6
8
  import { withFileLockSync } from "../file-lock.js";
7
9
  import { gsdRoot } from "../paths.js";
8
10
  import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
9
- import type { AuditEventEnvelope } from "./contracts.js";
11
+ import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent, type AuditEventEnvelope } from "./contracts.js";
10
12
 
11
13
  function auditLogPath(basePath: string): string {
12
14
  return join(gsdRoot(basePath), "audit", "events.jsonl");
@@ -25,6 +27,7 @@ export function buildAuditEnvelope(args: {
25
27
  payload?: Record<string, unknown>;
26
28
  }): AuditEventEnvelope {
27
29
  return {
30
+ version: CURRENT_UOK_CONTRACT_VERSION,
28
31
  eventId: randomUUID(),
29
32
  traceId: args.traceId,
30
33
  turnId: args.turnId,
@@ -39,6 +42,26 @@ export function buildAuditEnvelope(args: {
39
42
  export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
40
43
  // Drop writes from a turn superseded by timeout recovery / cancellation.
41
44
  if (isStaleWrite("uok-audit")) return;
45
+ const validation = validateAuditEvent(event);
46
+ if (!validation.ok) {
47
+ throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
48
+ }
49
+ const canonical = validation.value;
50
+
51
+ if (isDbAvailable()) {
52
+ try {
53
+ insertAuditEvent({
54
+ ...canonical,
55
+ payload: {
56
+ ...canonical.payload,
57
+ contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
58
+ },
59
+ });
60
+ } catch (err) {
61
+ throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ }
64
+
42
65
  try {
43
66
  ensureAuditDir(basePath);
44
67
  const path = auditLogPath(basePath);
@@ -52,18 +75,11 @@ export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope):
52
75
  withFileLockSync(
53
76
  path,
54
77
  () => {
55
- appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
78
+ appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
56
79
  },
57
80
  { onLocked: "skip" },
58
81
  );
59
82
  } catch {
60
83
  // Best-effort: audit writes must never break orchestration.
61
84
  }
62
-
63
- if (!isDbAvailable()) return;
64
- try {
65
- insertAuditEvent(event);
66
- } catch {
67
- // Projection failures are non-fatal while legacy readers are still active.
68
- }
69
85
  }
@@ -1,3 +1,9 @@
1
+ // GSD2 UOK Contract Types and Versioning
2
+
3
+ export const CURRENT_UOK_CONTRACT_VERSION = "1" as const;
4
+
5
+ export type UokContractVersion = "0" | typeof CURRENT_UOK_CONTRACT_VERSION;
6
+
1
7
  export type FailureClass =
2
8
  | "none"
3
9
  | "policy"
@@ -77,6 +83,7 @@ export interface TurnCloseoutRecord {
77
83
  }
78
84
 
79
85
  export interface TurnResult {
86
+ version?: UokContractVersion;
80
87
  traceId: string;
81
88
  turnId: string;
82
89
  iteration: number;
@@ -109,6 +116,7 @@ export interface DispatchExplanation {
109
116
  }
110
117
 
111
118
  export interface UokDispatchEnvelope {
119
+ version?: UokContractVersion;
112
120
  action: "dispatch" | "stop" | "skip";
113
121
  nodeKind?: UokNodeKind;
114
122
  unitType?: string;
@@ -130,6 +138,7 @@ export interface UokDispatchEnvelope {
130
138
  }
131
139
 
132
140
  export interface AuditEventEnvelope {
141
+ version?: UokContractVersion;
133
142
  eventId: string;
134
143
  traceId: string;
135
144
  turnId?: string;
@@ -199,3 +208,99 @@ export interface UokTurnObserver {
199
208
  ): void;
200
209
  onTurnResult(result: TurnResult): void;
201
210
  }
211
+
212
+ export interface ContractValidationIssue {
213
+ path: string;
214
+ message: string;
215
+ }
216
+
217
+ export interface ContractValidationResult<T> {
218
+ ok: boolean;
219
+ value: T;
220
+ issues: ContractValidationIssue[];
221
+ }
222
+
223
+ function isRecord(value: unknown): value is Record<string, unknown> {
224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
225
+ }
226
+
227
+ function normalizeVersion(value: unknown): UokContractVersion {
228
+ return value === CURRENT_UOK_CONTRACT_VERSION ? CURRENT_UOK_CONTRACT_VERSION : "0";
229
+ }
230
+
231
+ function requireString(
232
+ value: Record<string, unknown>,
233
+ key: string,
234
+ issues: ContractValidationIssue[],
235
+ ): void {
236
+ if (typeof value[key] !== "string" || value[key] === "") {
237
+ issues.push({ path: key, message: `${key} must be a non-empty string` });
238
+ }
239
+ }
240
+
241
+ function requireRecord(
242
+ value: Record<string, unknown>,
243
+ key: string,
244
+ issues: ContractValidationIssue[],
245
+ ): void {
246
+ if (!isRecord(value[key])) {
247
+ issues.push({ path: key, message: `${key} must be an object` });
248
+ }
249
+ }
250
+
251
+ export function normalizeTurnResult(value: TurnResult): TurnResult {
252
+ return { ...value, version: normalizeVersion(value.version) };
253
+ }
254
+
255
+ export function normalizeDispatchEnvelope(value: UokDispatchEnvelope): UokDispatchEnvelope {
256
+ return { ...value, version: normalizeVersion(value.version) };
257
+ }
258
+
259
+ export function normalizeAuditEvent(value: AuditEventEnvelope): AuditEventEnvelope {
260
+ return { ...value, version: normalizeVersion(value.version) };
261
+ }
262
+
263
+ export function validateTurnResult(value: TurnResult): ContractValidationResult<TurnResult> {
264
+ const normalized = normalizeTurnResult(value);
265
+ const record = normalized as unknown as Record<string, unknown>;
266
+ const issues: ContractValidationIssue[] = [];
267
+ requireString(record, "traceId", issues);
268
+ requireString(record, "turnId", issues);
269
+ if (!Number.isInteger(record.iteration)) {
270
+ issues.push({ path: "iteration", message: "iteration must be an integer" });
271
+ }
272
+ requireString(record, "status", issues);
273
+ requireString(record, "failureClass", issues);
274
+ if (!Array.isArray(record.phaseResults)) {
275
+ issues.push({ path: "phaseResults", message: "phaseResults must be an array" });
276
+ }
277
+ requireString(record, "startedAt", issues);
278
+ requireString(record, "finishedAt", issues);
279
+ return { ok: issues.length === 0, value: normalized, issues };
280
+ }
281
+
282
+ export function validateDispatchEnvelope(value: UokDispatchEnvelope): ContractValidationResult<UokDispatchEnvelope> {
283
+ const normalized = normalizeDispatchEnvelope(value);
284
+ const record = normalized as unknown as Record<string, unknown>;
285
+ const issues: ContractValidationIssue[] = [];
286
+ requireString(record, "action", issues);
287
+ requireRecord(record, "reason", issues);
288
+ if (isRecord(record.reason)) {
289
+ requireString(record.reason, "reasonCode", issues);
290
+ requireString(record.reason, "summary", issues);
291
+ }
292
+ return { ok: issues.length === 0, value: normalized, issues };
293
+ }
294
+
295
+ export function validateAuditEvent(value: AuditEventEnvelope): ContractValidationResult<AuditEventEnvelope> {
296
+ const normalized = normalizeAuditEvent(value);
297
+ const record = normalized as unknown as Record<string, unknown>;
298
+ const issues: ContractValidationIssue[] = [];
299
+ requireString(record, "eventId", issues);
300
+ requireString(record, "traceId", issues);
301
+ requireString(record, "category", issues);
302
+ requireString(record, "type", issues);
303
+ requireString(record, "ts", issues);
304
+ requireRecord(record, "payload", issues);
305
+ return { ok: issues.length === 0, value: normalized, issues };
306
+ }
@@ -1,3 +1,5 @@
1
+ // GSD2 UOK Dispatch Envelope Builder
2
+
1
3
  import type {
2
4
  DispatchExplanation,
3
5
  DispatchReasonCode,
@@ -5,6 +7,7 @@ import type {
5
7
  UokDispatchEnvelope,
6
8
  UokGraphNode,
7
9
  } from "./contracts.js";
10
+ import { CURRENT_UOK_CONTRACT_VERSION } from "./contracts.js";
8
11
 
9
12
  export interface BuildDispatchEnvelopeInput {
10
13
  action: UokDispatchEnvelope["action"];
@@ -22,6 +25,7 @@ export interface BuildDispatchEnvelopeInput {
22
25
 
23
26
  export function buildDispatchEnvelope(input: BuildDispatchEnvelopeInput): UokDispatchEnvelope {
24
27
  return {
28
+ version: CURRENT_UOK_CONTRACT_VERSION,
25
29
  action: input.action,
26
30
  nodeKind: input.node?.kind,
27
31
  unitType: input.unitType,
@@ -1,9 +1,12 @@
1
+ // GSD2 UOK Turn Observer and DB-Backed Lifecycle Emission
2
+
1
3
  import type {
2
4
  TurnCloseoutRecord,
3
5
  TurnContract,
4
6
  TurnResult,
5
7
  UokTurnObserver,
6
8
  } from "./contracts.js";
9
+ import { CURRENT_UOK_CONTRACT_VERSION, validateTurnResult } from "./contracts.js";
7
10
  import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
8
11
  import { writeTurnCloseoutGitRecord, writeTurnGitTransaction } from "./gitops.js";
9
12
  import { acquireWriterToken, nextWriteRecord, releaseWriterToken } from "./writer.js";
@@ -142,56 +145,68 @@ export function createTurnObserver(options: CreateTurnObserverOptions): UokTurnO
142
145
  },
143
146
 
144
147
  onTurnResult(result): void {
145
- const merged: TurnResult = {
146
- ...result,
147
- phaseResults: result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
148
+ const cleanup = (): void => {
149
+ if (writerToken) {
150
+ releaseWriterToken(options.basePath, writerToken);
151
+ }
152
+ writerToken = null;
153
+ current = null;
154
+ phaseResults.length = 0;
148
155
  };
149
156
 
150
- if (options.enableAudit) {
151
- emitUokAuditEvent(
152
- options.basePath,
153
- buildAuditEnvelope({
154
- traceId: merged.traceId,
155
- turnId: merged.turnId,
156
- category: "orchestration",
157
- type: "turn-result",
158
- payload: nextSequenceMetadata("audit", "append", {
159
- unitType: merged.unitType,
160
- unitId: merged.unitId,
161
- status: merged.status,
162
- failureClass: merged.failureClass,
163
- error: merged.error,
164
- phaseCount: merged.phaseResults.length,
165
- }),
166
- }),
167
- );
168
- }
169
-
170
- if (options.enableGitops) {
171
- const closeout: TurnCloseoutRecord = merged.closeout ?? {
172
- traceId: merged.traceId,
173
- turnId: merged.turnId,
174
- unitType: merged.unitType,
175
- unitId: merged.unitId,
176
- status: merged.status,
177
- failureClass: merged.failureClass,
178
- gitAction: options.gitAction,
179
- gitPushed: options.gitPush,
180
- finishedAt: merged.finishedAt,
157
+ try {
158
+ const merged: TurnResult = {
159
+ ...result,
160
+ version: CURRENT_UOK_CONTRACT_VERSION,
161
+ phaseResults: Array.isArray(result.phaseResults) && result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
181
162
  };
182
- writeTurnCloseoutGitRecord(
183
- options.basePath,
184
- closeout,
185
- nextSequenceMetadata("gitops", "update", { action: "record" }),
186
- );
187
- }
163
+ const validation = validateTurnResult(merged);
164
+ if (!validation.ok) {
165
+ throw new Error(`Invalid UOK turn result: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
166
+ }
188
167
 
189
- if (writerToken) {
190
- releaseWriterToken(options.basePath, writerToken);
168
+ if (options.enableAudit) {
169
+ emitUokAuditEvent(
170
+ options.basePath,
171
+ buildAuditEnvelope({
172
+ traceId: validation.value.traceId,
173
+ turnId: validation.value.turnId,
174
+ category: "orchestration",
175
+ type: "turn-result",
176
+ payload: nextSequenceMetadata("audit", "append", {
177
+ contractVersion: validation.value.version,
178
+ unitType: validation.value.unitType,
179
+ unitId: validation.value.unitId,
180
+ status: validation.value.status,
181
+ failureClass: validation.value.failureClass,
182
+ error: validation.value.error,
183
+ phaseCount: validation.value.phaseResults.length,
184
+ }),
185
+ }),
186
+ );
187
+ }
188
+
189
+ if (options.enableGitops) {
190
+ const closeout: TurnCloseoutRecord = merged.closeout ?? {
191
+ traceId: merged.traceId,
192
+ turnId: merged.turnId,
193
+ unitType: merged.unitType,
194
+ unitId: merged.unitId,
195
+ status: merged.status,
196
+ failureClass: merged.failureClass,
197
+ gitAction: options.gitAction,
198
+ gitPushed: options.gitPush,
199
+ finishedAt: merged.finishedAt,
200
+ };
201
+ writeTurnCloseoutGitRecord(
202
+ options.basePath,
203
+ closeout,
204
+ nextSequenceMetadata("gitops", "update", { action: "record" }),
205
+ );
206
+ }
207
+ } finally {
208
+ cleanup();
191
209
  }
192
- writerToken = null;
193
- current = null;
194
- phaseResults.length = 0;
195
210
  },
196
211
  };
197
212
  }
@@ -0,0 +1,158 @@
1
+ // GSD2 UOK Timeline Reconstruction from Authoritative DB Records
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ import { _getAdapter, isDbAvailable } from "../gsd-db.js";
7
+ import { gsdRoot } from "../paths.js";
8
+
9
+ export interface TurnTimelineFilter {
10
+ traceId?: string;
11
+ turnId?: string;
12
+ }
13
+
14
+ export interface TurnTimelineEntry {
15
+ source: "audit_events" | "unit_dispatches" | "turn_git_transactions" | "audit_jsonl";
16
+ ts: string;
17
+ traceId?: string;
18
+ turnId?: string | null;
19
+ type: string;
20
+ payload: Record<string, unknown>;
21
+ }
22
+
23
+ export interface TurnTimeline {
24
+ authoritative: "db" | "degraded-fallback";
25
+ degraded: boolean;
26
+ entries: TurnTimelineEntry[];
27
+ }
28
+
29
+ function parseJsonRecord(value: unknown): Record<string, unknown> {
30
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
31
+ return value as Record<string, unknown>;
32
+ }
33
+ if (typeof value !== "string" || value.trim() === "") return {};
34
+ try {
35
+ const parsed = JSON.parse(value) as unknown;
36
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
37
+ ? parsed as Record<string, unknown>
38
+ : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function matchesFilter(entry: Pick<TurnTimelineEntry, "traceId" | "turnId">, filter: TurnTimelineFilter): boolean {
45
+ if (filter.traceId && entry.traceId !== filter.traceId) return false;
46
+ if (filter.turnId && entry.turnId !== filter.turnId) return false;
47
+ return true;
48
+ }
49
+
50
+ function byTimestamp(a: TurnTimelineEntry, b: TurnTimelineEntry): number {
51
+ return a.ts.localeCompare(b.ts);
52
+ }
53
+
54
+ function readDbTimeline(filter: TurnTimelineFilter): TurnTimelineEntry[] {
55
+ const db = _getAdapter();
56
+ if (!db) return [];
57
+ const entries: TurnTimelineEntry[] = [];
58
+ const where: string[] = [];
59
+ const params: Record<string, string> = {};
60
+ if (filter.traceId) {
61
+ where.push("trace_id = :trace_id");
62
+ params[":trace_id"] = filter.traceId;
63
+ }
64
+ if (filter.turnId) {
65
+ where.push("turn_id = :turn_id");
66
+ params[":turn_id"] = filter.turnId;
67
+ }
68
+ const suffix = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
69
+
70
+ const auditRows = db.prepare(
71
+ `SELECT trace_id, turn_id, type, ts, payload_json FROM audit_events${suffix}`,
72
+ ).all(params) as Array<{ trace_id: string; turn_id: string | null; type: string; ts: string; payload_json: string }>;
73
+ for (const row of auditRows) {
74
+ entries.push({
75
+ source: "audit_events",
76
+ ts: row.ts,
77
+ traceId: row.trace_id,
78
+ turnId: row.turn_id,
79
+ type: row.type,
80
+ payload: parseJsonRecord(row.payload_json),
81
+ });
82
+ }
83
+
84
+ const dispatchRows = db.prepare(
85
+ `SELECT trace_id, turn_id, unit_type, unit_id, status, started_at, ended_at, exit_reason,
86
+ error_summary, retry_after_ms, attempt_n, max_attempts
87
+ FROM unit_dispatches${suffix}`,
88
+ ).all(params) as Array<Record<string, unknown>>;
89
+ for (const row of dispatchRows) {
90
+ entries.push({
91
+ source: "unit_dispatches",
92
+ ts: String(row.ended_at ?? row.started_at ?? ""),
93
+ traceId: String(row.trace_id ?? ""),
94
+ turnId: typeof row.turn_id === "string" ? row.turn_id : null,
95
+ type: `dispatch-${String(row.status ?? "unknown")}`,
96
+ payload: { ...row },
97
+ });
98
+ }
99
+
100
+ const gitRows = db.prepare(
101
+ `SELECT trace_id, turn_id, unit_type, unit_id, stage, action, push, status, error,
102
+ metadata_json, updated_at
103
+ FROM turn_git_transactions${suffix}`,
104
+ ).all(params) as Array<Record<string, unknown>>;
105
+ for (const row of gitRows) {
106
+ entries.push({
107
+ source: "turn_git_transactions",
108
+ ts: String(row.updated_at ?? ""),
109
+ traceId: String(row.trace_id ?? ""),
110
+ turnId: typeof row.turn_id === "string" ? row.turn_id : null,
111
+ type: `gitops-${String(row.stage ?? "unknown")}`,
112
+ payload: {
113
+ ...row,
114
+ metadata: parseJsonRecord(row.metadata_json),
115
+ },
116
+ });
117
+ }
118
+
119
+ return entries.filter((entry) => entry.ts !== "").sort(byTimestamp);
120
+ }
121
+
122
+ function readJsonlTimeline(basePath: string, filter: TurnTimelineFilter): TurnTimelineEntry[] {
123
+ const path = join(gsdRoot(basePath), "audit", "events.jsonl");
124
+ if (!existsSync(path)) return [];
125
+ return readFileSync(path, "utf-8")
126
+ .split("\n")
127
+ .filter(Boolean)
128
+ .map((line): TurnTimelineEntry | null => {
129
+ const event = parseJsonRecord(line);
130
+ const entry: TurnTimelineEntry = {
131
+ source: "audit_jsonl",
132
+ ts: String(event.ts ?? ""),
133
+ traceId: typeof event.traceId === "string" ? event.traceId : undefined,
134
+ turnId: typeof event.turnId === "string" ? event.turnId : null,
135
+ type: String(event.type ?? "audit"),
136
+ payload: parseJsonRecord(event.payload),
137
+ };
138
+ return entry.ts && matchesFilter(entry, filter) ? entry : null;
139
+ })
140
+ .filter((entry): entry is TurnTimelineEntry => entry !== null)
141
+ .sort(byTimestamp);
142
+ }
143
+
144
+ export function buildTurnTimeline(basePath: string, filter: TurnTimelineFilter = {}): TurnTimeline {
145
+ if (isDbAvailable()) {
146
+ return {
147
+ authoritative: "db",
148
+ degraded: false,
149
+ entries: readDbTimeline(filter),
150
+ };
151
+ }
152
+
153
+ return {
154
+ authoritative: "degraded-fallback",
155
+ degraded: true,
156
+ entries: readJsonlTimeline(basePath, filter),
157
+ };
158
+ }