gsd-pi 2.70.1-dev.6504106 → 2.70.1-dev.7d1d9d3

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 (51) hide show
  1. package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
  2. package/dist/resources/extensions/gsd/file-lock.js +60 -0
  3. package/dist/resources/extensions/gsd/workflow-events.js +25 -13
  4. package/dist/web/standalone/.next/BUILD_ID +1 -1
  5. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  6. package/dist/web/standalone/.next/build-manifest.json +2 -2
  7. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/web/standalone/.next/required-server-files.json +1 -1
  9. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/index.html +1 -1
  26. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  33. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  34. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  35. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  36. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  37. package/dist/web/standalone/server.js +1 -1
  38. package/package.json +1 -1
  39. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +78 -0
  40. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  42. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -3
  43. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  44. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +98 -0
  45. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +12 -3
  46. package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
  47. package/src/resources/extensions/gsd/file-lock.ts +59 -0
  48. package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
  49. package/src/resources/extensions/gsd/workflow-events.ts +34 -25
  50. /package/dist/web/standalone/.next/static/{T3hWbQ-WQBDEIGoaOOfEo → 52NuiWbmUzXpzxaTEDopT}/_buildManifest.js +0 -0
  51. /package/dist/web/standalone/.next/static/{T3hWbQ-WQBDEIGoaOOfEo → 52NuiWbmUzXpzxaTEDopT}/_ssgManifest.js +0 -0
@@ -34,6 +34,7 @@ import {
34
34
  import { injectContext } from "./context-injector.js";
35
35
  import type { WorkflowDefinition, StepDefinition } from "./definition-loader.js";
36
36
  import { parseUnitId } from "./unit-id.js";
37
+ import { withFileLock } from "./file-lock.js";
37
38
 
38
39
  /** Read and parse the frozen DEFINITION.yaml from a run directory. */
39
40
  export function readFrozenDefinition(runDir: string): WorkflowDefinition {
@@ -179,24 +180,28 @@ export class CustomWorkflowEngine implements WorkflowEngine {
179
180
  state: EngineState,
180
181
  completedStep: CompletedStep,
181
182
  ): Promise<ReconcileResult> {
182
- // Re-read the graph from disk so we do not overwrite concurrent
183
- // workflow edits with a stale in-memory snapshot from deriveState().
184
- const graph = readGraph(this.runDir);
183
+ const graphPath = join(this.runDir, "GRAPH.yaml");
185
184
 
186
- // Extract stepId from "<workflowName>/<stepId>"
187
- const { milestone, slice, task } = parseUnitId(completedStep.unitId);
188
- const stepId = task ?? slice ?? milestone;
185
+ return await withFileLock(graphPath, () => {
186
+ // Re-read the graph from disk so we do not overwrite concurrent
187
+ // workflow edits with a stale in-memory snapshot from deriveState().
188
+ const graph = readGraph(this.runDir);
189
189
 
190
- const updatedGraph = markStepComplete(graph, stepId);
191
- writeGraph(this.runDir, updatedGraph);
190
+ // Extract stepId from "<workflowName>/<stepId>"
191
+ const { milestone, slice, task } = parseUnitId(completedStep.unitId);
192
+ const stepId = task ?? slice ?? milestone;
192
193
 
193
- const allDone = updatedGraph.steps.every(
194
- (s) => s.status === "complete" || s.status === "expanded",
195
- );
194
+ const updatedGraph = markStepComplete(graph, stepId);
195
+ writeGraph(this.runDir, updatedGraph);
196
196
 
197
- return {
198
- outcome: allDone ? "milestone-complete" : "continue",
199
- };
197
+ const allDone = updatedGraph.steps.every(
198
+ (s) => s.status === "complete" || s.status === "expanded",
199
+ );
200
+
201
+ return {
202
+ outcome: allDone ? "milestone-complete" : "continue",
203
+ };
204
+ });
200
205
  }
201
206
 
202
207
  /**
@@ -0,0 +1,59 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ function _require(name: string) {
4
+ try {
5
+ return require(name);
6
+ } catch {
7
+ try {
8
+ const gsdPiRequire = require("module").createRequire(
9
+ require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js")
10
+ );
11
+ return gsdPiRequire(name);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ }
17
+
18
+ export function withFileLockSync<T>(filePath: string, fn: () => T): T {
19
+ const lockfile = _require("proper-lockfile");
20
+ if (!lockfile) return fn();
21
+
22
+ if (!existsSync(filePath)) return fn();
23
+
24
+ try {
25
+ const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
26
+ try {
27
+ return fn();
28
+ } finally {
29
+ release();
30
+ }
31
+ } catch (err: any) {
32
+ if (err.code === "ELOCKED") {
33
+ // Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
34
+ return fn();
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ export async function withFileLock<T>(filePath: string, fn: () => Promise<T> | T): Promise<T> {
41
+ const lockfile = _require("proper-lockfile");
42
+ if (!lockfile) return await fn();
43
+
44
+ if (!existsSync(filePath)) return await fn();
45
+
46
+ try {
47
+ const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
48
+ try {
49
+ return await fn();
50
+ } finally {
51
+ await release();
52
+ }
53
+ } catch (err: any) {
54
+ if (err.code === "ELOCKED") {
55
+ return await fn();
56
+ }
57
+ throw err;
58
+ }
59
+ }
@@ -0,0 +1,103 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ import { withFileLock, withFileLockSync } from "../file-lock.ts";
9
+
10
+ const require = createRequire(import.meta.url);
11
+
12
+ function hasProperLockfile(): boolean {
13
+ try {
14
+ require("proper-lockfile");
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ test("withFileLockSync: executes callback when file does not exist", () => {
22
+ const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
23
+ try {
24
+ const missingPath = join(dir, "missing.txt");
25
+ let called = 0;
26
+ const result = withFileLockSync(missingPath, () => {
27
+ called++;
28
+ return "ok";
29
+ });
30
+
31
+ assert.equal(result, "ok");
32
+ assert.equal(called, 1, "callback should execute exactly once");
33
+ } finally {
34
+ rmSync(dir, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test("withFileLock: executes callback when file does not exist", async () => {
39
+ const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
40
+ try {
41
+ const missingPath = join(dir, "missing.txt");
42
+ let called = 0;
43
+ const result = await withFileLock(missingPath, async () => {
44
+ called++;
45
+ return "ok";
46
+ });
47
+
48
+ assert.equal(result, "ok");
49
+ assert.equal(called, 1, "callback should execute exactly once");
50
+ } finally {
51
+ rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ });
54
+
55
+ test("withFileLockSync: falls back to unlocked callback on ELOCKED", () => {
56
+ if (!hasProperLockfile() || process.platform === "win32") {
57
+ return;
58
+ }
59
+
60
+ const lockfile = require("proper-lockfile");
61
+ const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
62
+ const filePath = join(dir, "locked.jsonl");
63
+ writeFileSync(filePath, "{}\n", "utf-8");
64
+
65
+ const release = lockfile.lockSync(filePath, { retries: 0, stale: 10000 });
66
+ try {
67
+ let called = 0;
68
+ const result = withFileLockSync(filePath, () => {
69
+ called++;
70
+ return "fallback-ok";
71
+ });
72
+ assert.equal(result, "fallback-ok");
73
+ assert.equal(called, 1, "callback should run even when lock acquisition fails");
74
+ } finally {
75
+ release();
76
+ rmSync(dir, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test("withFileLock: falls back to unlocked callback on ELOCKED", async () => {
81
+ if (!hasProperLockfile() || process.platform === "win32") {
82
+ return;
83
+ }
84
+
85
+ const lockfile = require("proper-lockfile");
86
+ const dir = mkdtempSync(join(tmpdir(), "gsd-file-lock-test-"));
87
+ const filePath = join(dir, "locked.jsonl");
88
+ writeFileSync(filePath, "{}\n", "utf-8");
89
+
90
+ const release = await lockfile.lock(filePath, { retries: 0, stale: 10000 });
91
+ try {
92
+ let called = 0;
93
+ const result = await withFileLock(filePath, async () => {
94
+ called++;
95
+ return "fallback-ok";
96
+ });
97
+ assert.equal(result, "fallback-ok");
98
+ assert.equal(called, 1, "callback should run even when lock acquisition fails");
99
+ } finally {
100
+ await release();
101
+ rmSync(dir, { recursive: true, force: true });
102
+ }
103
+ });
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { atomicWriteSync } from "./atomic-write.js";
5
+ import { withFileLockSync } from "./file-lock.js";
5
6
  import { logWarning } from "./workflow-logger.js";
6
7
 
7
8
  // ─── Session ID ───────────────────────────────────────────────────────────
@@ -127,31 +128,39 @@ export function compactMilestoneEvents(
127
128
  const logPath = join(basePath, ".gsd", "event-log.jsonl");
128
129
  const archivePath = join(basePath, ".gsd", `event-log-${milestoneId}.jsonl.archived`);
129
130
 
130
- const allEvents = readEvents(logPath);
131
- const toArchive = allEvents.filter(
132
- (e) => (e.params as { milestoneId?: string }).milestoneId === milestoneId,
133
- );
134
- const remaining = allEvents.filter(
135
- (e) => (e.params as { milestoneId?: string }).milestoneId !== milestoneId,
136
- );
131
+ return withFileLockSync(logPath, () => {
132
+ const allEvents = readEvents(logPath);
133
+
134
+ // Single-pass partition to halve the work (per reviewer agent)
135
+ const toArchive: WorkflowEvent[] = [];
136
+ const remaining: WorkflowEvent[] = [];
137
+
138
+ for (const e of allEvents) {
139
+ if ((e.params as { milestoneId?: string }).milestoneId === milestoneId) {
140
+ toArchive.push(e);
141
+ } else {
142
+ remaining.push(e);
143
+ }
144
+ }
137
145
 
138
- if (toArchive.length === 0) {
139
- return { archived: 0 };
140
- }
146
+ if (toArchive.length === 0) {
147
+ return { archived: 0 };
148
+ }
141
149
 
142
- // Write archived events to .jsonl.archived file (crash-safe)
143
- atomicWriteSync(
144
- archivePath,
145
- toArchive.map((e) => JSON.stringify(e)).join("\n") + "\n",
146
- );
147
-
148
- // Truncate active log to remaining events only
149
- atomicWriteSync(
150
- logPath,
151
- remaining.length > 0
152
- ? remaining.map((e) => JSON.stringify(e)).join("\n") + "\n"
153
- : "",
154
- );
155
-
156
- return { archived: toArchive.length };
150
+ // Write archived events to .jsonl.archived file (crash-safe)
151
+ atomicWriteSync(
152
+ archivePath,
153
+ toArchive.map((e) => JSON.stringify(e)).join("\n") + "\n",
154
+ );
155
+
156
+ // Truncate active log to remaining events only
157
+ atomicWriteSync(
158
+ logPath,
159
+ remaining.length > 0
160
+ ? remaining.map((e) => JSON.stringify(e)).join("\n") + "\n"
161
+ : "",
162
+ );
163
+
164
+ return { archived: toArchive.length };
165
+ });
157
166
  }