gcf-harness-aif-ext 0.6.0 → 0.6.1
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 +3 -1
- package/commands/harness.js +2 -0
- package/commands/run.js +66 -11
- package/extension.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The harness turns policy-as-text into an executable pipeline. It has **no memory
|
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
14
|
-
- [`gcf-aif-ext`](../gcf-aif-ext) `^0.5.
|
|
14
|
+
- [`gcf-aif-ext`](../gcf-aif-ext) `^0.5.3` installed, with a `gcf/` tree in the project — declared as a peer dependency, and `gcf-harness init` refuses when the `gcf` command is not registered
|
|
15
15
|
- Claude Code 2.1.270+ (hooks, dynamic workflows) — the only supported runtime
|
|
16
16
|
- AI Factory 2.x, Node.js ≥ 22.14
|
|
17
17
|
|
|
@@ -47,6 +47,8 @@ Updating keeps the hook entry valid — it points into the extension's install d
|
|
|
47
47
|
|
|
48
48
|
0.5.1 is a fix release. During a run, `gcf/config.yml` is always readable, and a Grep inside `gcf/` reads only readset files. An import breaks a `depends:` cycle the pair checks closed and asks a D1 about it. It cuts a section longer than a segment at paragraphs, and lens and pair prompts carry a window around the quote. `/gcf-run` checks the plan against each pillar's invariant and asks each pillar's review question over the diff. Use it with core 0.4.1, which brings the parent feature into the readset.
|
|
49
49
|
|
|
50
|
+
0.6.1 is a fix release, to update together with core 0.5.3. The run lock `.gcf-session/runs/<id>.lock` is created exclusively, and a lock left by a dead run is taken over; while it is live, the gate refuses the item to another run and the core refuses any move on it, `/gcf-run` on main included. A session whose log cannot open ends as an error and releases its claim.
|
|
51
|
+
|
|
50
52
|
0.6.0 adds `ai-factory gcf-harness run` (a batch at once, a worktree and an agent session per item, then the integrator) and the `--worktree` mode of `/gcf-run`, whose session ritual now integrates the verified branches a dead run left. It requires `gcf-aif-ext` 0.5.0 (`gcf worktree`, `gcf integrate`, `gcf verify`) — update both together and run `ai-factory gcf init`. The DoD of `/gcf-run` is `ai-factory gcf verify <id>`, and the item's `live:` delta is written before it.
|
|
51
53
|
|
|
52
54
|
0.4.1 is a patch release. `/gcf-run` now commits the implementation first, then flips the card to `built` in a second commit, with `built_at` set to the implementation commit's hash. Update the core to 0.3.1 together with the harness: without it, an audit run before a verdict makes the pre-commit gate refuse that verdict.
|
package/commands/harness.js
CHANGED
|
@@ -27,6 +27,8 @@ function itemProblem(id, item, report) {
|
|
|
27
27
|
return `${id} is ${item.status} — only ready items are driven`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
if (item.running) return `${id} is driven by another harness run — wait until it ends`;
|
|
31
|
+
|
|
30
32
|
// A child of a feature paused after a death does not start: the decision may kill it.
|
|
31
33
|
const parent = item.parent ? report.items.find((p) => p.id === item.parent) : null;
|
|
32
34
|
if (parent?.status === 'awaiting-decision') {
|
package/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// worktree, and the block is recorded on main — a worktree never writes gcf/ status.
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import { mkdir, open, readFile, rm, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_AGENT = 'claude -p';
|
|
@@ -57,23 +57,73 @@ async function outcomeOf(dir) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Whether a pid is a live process; EPERM is alive under another user. */
|
|
61
|
+
function alive(pid) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return e.code === 'EPERM';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* One harness run per item at a time: `.gcf-session/runs/<id>.lock` holds the pid of the run
|
|
62
|
-
* that drives it. A lock whose process is gone is taken over
|
|
73
|
+
* that drives it, created exclusively. A lock whose process is gone is taken over; the core
|
|
74
|
+
* refuses moves on the item while the lock is live. Returns the release, or null.
|
|
63
75
|
*/
|
|
64
76
|
async function claim(logs, id) {
|
|
65
77
|
const file = path.join(logs, `${id}.lock`);
|
|
78
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
79
|
+
if (await create(file)) return () => rm(file, { force: true });
|
|
80
|
+
const text = await readFile(file, 'utf8').catch(() => null);
|
|
81
|
+
if (text === null) continue; // released meanwhile
|
|
82
|
+
const pid = Number(text.trim());
|
|
83
|
+
if (pid === process.pid) return () => rm(file, { force: true });
|
|
84
|
+
if (!(await dead(file, pid))) return null;
|
|
85
|
+
await clearDead(file, text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create the lock with this pid, or false when it exists. */
|
|
92
|
+
async function create(file) {
|
|
66
93
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch {
|
|
71
|
-
|
|
94
|
+
await writeFile(file, `${process.pid}\n`, { flag: 'wx' });
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e.code !== 'EEXIST') throw e;
|
|
99
|
+
|
|
100
|
+
return false;
|
|
72
101
|
}
|
|
102
|
+
}
|
|
73
103
|
|
|
74
|
-
|
|
104
|
+
/** A lock whose pid is gone, or that never got one and is old: its creator died writing it. */
|
|
105
|
+
async function dead(file, pid) {
|
|
106
|
+
if (Number.isInteger(pid) && pid > 0) return !alive(pid);
|
|
107
|
+
const age = await stat(file).then(
|
|
108
|
+
(s) => Date.now() - s.mtimeMs,
|
|
109
|
+
() => 0,
|
|
110
|
+
);
|
|
75
111
|
|
|
76
|
-
return
|
|
112
|
+
return age > 10000;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove a dead lock under `<lock>.break`, as the core's main lock does: two runs that judged
|
|
117
|
+
* the same lock dead cannot both remove it, so neither deletes the other's fresh lock.
|
|
118
|
+
*/
|
|
119
|
+
async function clearDead(file, text) {
|
|
120
|
+
const breaker = `${file}.break`;
|
|
121
|
+
if (!(await create(breaker))) return;
|
|
122
|
+
try {
|
|
123
|
+
if ((await readFile(file, 'utf8').catch(() => null)) === text) await rm(file, { force: true });
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(breaker, { force: true });
|
|
126
|
+
}
|
|
77
127
|
}
|
|
78
128
|
|
|
79
129
|
/** Whether the tip of the worktree's branch carries `GCF-Verified: <id>`. */
|
|
@@ -112,11 +162,15 @@ async function prepare(ids, core, logs) {
|
|
|
112
162
|
|
|
113
163
|
/** One session per item, its output in `.gcf-session/runs/<id>.log`. */
|
|
114
164
|
async function session(r, agent, logs) {
|
|
115
|
-
|
|
165
|
+
let log = null;
|
|
116
166
|
try {
|
|
167
|
+
log = await open(path.join(logs, `${r.id}.log`), 'a');
|
|
168
|
+
|
|
117
169
|
return { ...r, code: await startAgent(agent, r.id, r.dir, log.fd) };
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { ...r, code: null, error: e.message };
|
|
118
172
|
} finally {
|
|
119
|
-
await log
|
|
173
|
+
await log?.close();
|
|
120
174
|
await r.release();
|
|
121
175
|
}
|
|
122
176
|
}
|
|
@@ -142,6 +196,7 @@ async function recordBlock(r, outcome, core) {
|
|
|
142
196
|
/** The result line of an item the integrator handled, or why it has none. */
|
|
143
197
|
function resultOf(run, integrated) {
|
|
144
198
|
const done = integrated.find((x) => x.id === run.id);
|
|
199
|
+
if (!done && run.error) return { id: run.id, result: 'error', error: run.error };
|
|
145
200
|
if (!done)
|
|
146
201
|
return {
|
|
147
202
|
id: run.id,
|
package/extension.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
|
|
3
3
|
"name": "gcf-harness-aif-ext",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/gcf-run",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gcf-harness-aif-ext",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"prepack": "node -e \"const p=require('./package.json'),e=require('./extension.json');if(p.version!==e.version)throw new Error('version mismatch: package.json '+p.version+' vs extension.json '+e.version)\""
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"gcf-aif-ext": "^0.5.
|
|
31
|
+
"gcf-aif-ext": "^0.5.3"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"extension.json",
|