gcf-harness-aif-ext 0.6.0 → 0.7.0
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 +5 -1
- package/commands/harness.js +25 -8
- package/commands/run.js +156 -28
- 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.
|
|
14
|
+
- [`gcf-aif-ext`](../gcf-aif-ext) `^0.6.0` 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,10 @@ 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.7.0, to update together with core 0.6.0: an item's `model:` and `effort:` (`gcf item new --model <m> --effort <e>`) go to its agent as `--model <m> --effort <e>`, read from `gcf board --json`; a value that is not one plain word fails that item and starts no session. `--timeout <minutes>` (120 by default) stops a session that runs longer, with every process it started; its item stays ready, and its run lock is freed. The gate counts the items other runs drive now: their places under the WIP limit, and their systems — a board starts one run per click. Requires `gcf-aif-ext` 0.6.
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
50
54
|
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
55
|
|
|
52
56
|
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
|
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { run, cliCore, DEFAULT_AGENT } from './run.js';
|
|
10
|
+
import { run, cliCore, DEFAULT_AGENT, DEFAULT_TIMEOUT_MIN } from './run.js';
|
|
11
11
|
|
|
12
12
|
const EXT_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
13
|
const MARKER = 'gcf-readset-guard';
|
|
@@ -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') {
|
|
@@ -47,20 +49,26 @@ function itemProblem(id, item, report) {
|
|
|
47
49
|
return null;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Why the batch as a whole may not go, or null. `elsewhere` are the items other runs drive now:
|
|
54
|
+
* a board starts one run per click, so each run sees the others only through their locks.
|
|
55
|
+
*/
|
|
56
|
+
function batchProblem(items, wip, elsewhere = []) {
|
|
52
57
|
// Only an item that ends waiting for a verdict takes a WIP place; `wip.over` is false at
|
|
53
|
-
// count === limit, and this run adds every such item of the batch.
|
|
58
|
+
// count === limit, and this run adds every such item of the batch — and of the other runs.
|
|
54
59
|
const waiting = items.filter((i) => i.gate === 'verdict').length;
|
|
55
|
-
|
|
60
|
+
const coming = elsewhere.filter((i) => i.gate === 'verdict').length;
|
|
61
|
+
if (waiting && wip.count + coming + waiting > wip.limit) {
|
|
62
|
+
const others = coming ? ` + ${coming} in other runs` : '';
|
|
63
|
+
|
|
56
64
|
return (
|
|
57
|
-
`WIP: ${wip.count} awaiting a verdict + ${waiting} in this run exceeds the
|
|
58
|
-
|
|
65
|
+
`WIP: ${wip.count} awaiting a verdict${others} + ${waiting} in this run exceeds the ` +
|
|
66
|
+
`limit of ${wip.limit} — playtest first`
|
|
59
67
|
);
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
for (let i = 0; i < items.length; i++) {
|
|
63
|
-
for (const other of items.slice(i + 1)) {
|
|
71
|
+
for (const other of [...items.slice(i + 1), ...elsewhere]) {
|
|
64
72
|
const shared = items[i].system.filter((s) => other.system.includes(s));
|
|
65
73
|
// `parallel: ok` on both is the operator's explicit permission (Methodology §7).
|
|
66
74
|
const allowed = items[i].parallel === 'ok' && other.parallel === 'ok';
|
|
@@ -102,6 +110,7 @@ export function gate(report, ids) {
|
|
|
102
110
|
batchProblem(
|
|
103
111
|
ids.map((id) => byId.get(id)),
|
|
104
112
|
report.wip,
|
|
113
|
+
report.items.filter((c) => c.running && !ids.includes(c.id)),
|
|
105
114
|
),
|
|
106
115
|
);
|
|
107
116
|
}
|
|
@@ -247,6 +256,11 @@ export function register(program) {
|
|
|
247
256
|
'the agent command; it gets "/gcf-run <id> --worktree"',
|
|
248
257
|
process.env.GCF_RUN_AGENT ?? DEFAULT_AGENT,
|
|
249
258
|
)
|
|
259
|
+
.option(
|
|
260
|
+
'--timeout <minutes>',
|
|
261
|
+
'stop a session that runs longer; its item stays ready',
|
|
262
|
+
String(DEFAULT_TIMEOUT_MIN),
|
|
263
|
+
)
|
|
250
264
|
.option('--json', 'machine output')
|
|
251
265
|
.action(runItems);
|
|
252
266
|
}
|
|
@@ -271,9 +285,12 @@ const failed = (r) =>
|
|
|
271
285
|
|
|
272
286
|
async function runItems(ids, opts) {
|
|
273
287
|
try {
|
|
288
|
+
const minutes = Number(opts.timeout);
|
|
289
|
+
if (!(minutes > 0)) throw new Error('--timeout is a number of minutes above 0');
|
|
274
290
|
const r = await run(process.cwd(), [...new Set(ids)], {
|
|
275
291
|
agent: opts.agent,
|
|
276
292
|
gate: (batch) => gate(auditReport(), batch),
|
|
293
|
+
timeoutMs: minutes * 60_000,
|
|
277
294
|
});
|
|
278
295
|
if (opts.json) console.log(JSON.stringify(r, null, 2));
|
|
279
296
|
else printRun(r);
|
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';
|
|
@@ -31,16 +31,69 @@ function json(res, what) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// The core's rule for `item new`: a leading hyphen would read as a flag.
|
|
35
|
+
const RUN_WORD = /^[A-Za-z0-9][\w.-]*$/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The agent command of one item: its `model:` and `effort:` become `--model` and `--effort`,
|
|
39
|
+
* the flags of `claude`. The command goes through a shell, and an item file can be edited by
|
|
40
|
+
* hand: a value that is not one plain word is refused.
|
|
41
|
+
*/
|
|
42
|
+
export function agentFor(agent, item) {
|
|
43
|
+
const flags = [];
|
|
44
|
+
for (const k of ['model', 'effort']) {
|
|
45
|
+
const v = item?.[k];
|
|
46
|
+
if (v === null || v === undefined) continue;
|
|
47
|
+
if (!RUN_WORD.test(String(v))) throw new Error(`${k}: ${v} is not one plain word`);
|
|
48
|
+
flags.push(`--${k} ${v}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [agent, ...flags].join(' ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const WIN = process.platform === 'win32';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stop a session and everything it started: the shell, the agent, its tools. Windows has no
|
|
58
|
+
* process groups to signal, `taskkill /T` walks the tree; elsewhere the session leads its own
|
|
59
|
+
* group (`detached`), and the group gets the signal.
|
|
60
|
+
*/
|
|
61
|
+
function killTree(child) {
|
|
62
|
+
if (WIN) spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
63
|
+
else {
|
|
64
|
+
try {
|
|
65
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
66
|
+
} catch {
|
|
67
|
+
// Gone already.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One agent session in the item's worktree; resolves with its exit code, or `timeout` when it
|
|
74
|
+
* ran past `timeoutMs`: a hung session would hold the item's run lock, and every move on it,
|
|
75
|
+
* for good.
|
|
76
|
+
*/
|
|
77
|
+
function startAgent(agent, id, dir, { fd, timeoutMs }) {
|
|
36
78
|
return new Promise((resolve) => {
|
|
37
79
|
const child = spawn(`${agent} "/gcf-run ${id} --worktree"`, {
|
|
38
80
|
cwd: dir,
|
|
39
81
|
shell: true,
|
|
82
|
+
detached: !WIN,
|
|
83
|
+
windowsHide: true,
|
|
40
84
|
stdio: ['ignore', fd, fd],
|
|
41
85
|
});
|
|
42
|
-
|
|
43
|
-
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
killTree(child);
|
|
88
|
+
resolve('timeout');
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
const end = (code) => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(code);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
child.on('error', () => end(-1));
|
|
96
|
+
child.on('exit', (code) => end(code ?? -1));
|
|
44
97
|
});
|
|
45
98
|
}
|
|
46
99
|
|
|
@@ -57,23 +110,73 @@ async function outcomeOf(dir) {
|
|
|
57
110
|
}
|
|
58
111
|
}
|
|
59
112
|
|
|
113
|
+
/** Whether a pid is a live process; EPERM is alive under another user. */
|
|
114
|
+
function alive(pid) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, 0);
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return e.code === 'EPERM';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
/**
|
|
61
125
|
* 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
|
|
126
|
+
* that drives it, created exclusively. A lock whose process is gone is taken over; the core
|
|
127
|
+
* refuses moves on the item while the lock is live. Returns the release, or null.
|
|
63
128
|
*/
|
|
64
129
|
async function claim(logs, id) {
|
|
65
130
|
const file = path.join(logs, `${id}.lock`);
|
|
131
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
132
|
+
if (await create(file)) return () => rm(file, { force: true });
|
|
133
|
+
const text = await readFile(file, 'utf8').catch(() => null);
|
|
134
|
+
if (text === null) continue; // released meanwhile
|
|
135
|
+
const pid = Number(text.trim());
|
|
136
|
+
if (pid === process.pid) return () => rm(file, { force: true });
|
|
137
|
+
if (!(await dead(file, pid))) return null;
|
|
138
|
+
await clearDead(file, text);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Create the lock with this pid, or false when it exists. */
|
|
145
|
+
async function create(file) {
|
|
66
146
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch {
|
|
71
|
-
|
|
147
|
+
await writeFile(file, `${process.pid}\n`, { flag: 'wx' });
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if (e.code !== 'EEXIST') throw e;
|
|
152
|
+
|
|
153
|
+
return false;
|
|
72
154
|
}
|
|
155
|
+
}
|
|
73
156
|
|
|
74
|
-
|
|
157
|
+
/** A lock whose pid is gone, or that never got one and is old: its creator died writing it. */
|
|
158
|
+
async function dead(file, pid) {
|
|
159
|
+
if (Number.isInteger(pid) && pid > 0) return !alive(pid);
|
|
160
|
+
const age = await stat(file).then(
|
|
161
|
+
(s) => Date.now() - s.mtimeMs,
|
|
162
|
+
() => 0,
|
|
163
|
+
);
|
|
75
164
|
|
|
76
|
-
return
|
|
165
|
+
return age > 10000;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Remove a dead lock under `<lock>.break`, as the core's main lock does: two runs that judged
|
|
170
|
+
* the same lock dead cannot both remove it, so neither deletes the other's fresh lock.
|
|
171
|
+
*/
|
|
172
|
+
async function clearDead(file, text) {
|
|
173
|
+
const breaker = `${file}.break`;
|
|
174
|
+
if (!(await create(breaker))) return;
|
|
175
|
+
try {
|
|
176
|
+
if ((await readFile(file, 'utf8').catch(() => null)) === text) await rm(file, { force: true });
|
|
177
|
+
} finally {
|
|
178
|
+
await rm(breaker, { force: true });
|
|
179
|
+
}
|
|
77
180
|
}
|
|
78
181
|
|
|
79
182
|
/** Whether the tip of the worktree's branch carries `GCF-Verified: <id>`. */
|
|
@@ -86,8 +189,11 @@ function verified(dir, id) {
|
|
|
86
189
|
return (res.stdout ?? '').split('\n').some((v) => v.trim() === id);
|
|
87
190
|
}
|
|
88
191
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The worktree and the agent command of each item, claimed for this run; an item that cannot
|
|
194
|
+
* start is an error. `agentOf(id)` gives the command.
|
|
195
|
+
*/
|
|
196
|
+
async function prepare(ids, core, logs, agentOf) {
|
|
91
197
|
const ready = [];
|
|
92
198
|
const failed = [];
|
|
93
199
|
for (const id of ids) {
|
|
@@ -98,9 +204,10 @@ async function prepare(ids, core, logs) {
|
|
|
98
204
|
}
|
|
99
205
|
|
|
100
206
|
try {
|
|
207
|
+
const agent = agentOf(id);
|
|
101
208
|
const wt = json(await core(['worktree', id, '--json']), `worktree ${id}`);
|
|
102
209
|
if (wt.sync) throw new Error(wt.sync);
|
|
103
|
-
ready.push({ id, dir: wt.path, release });
|
|
210
|
+
ready.push({ id, dir: wt.path, agent, release });
|
|
104
211
|
} catch (e) {
|
|
105
212
|
await release();
|
|
106
213
|
failed.push({ id, result: 'error', error: e.message });
|
|
@@ -111,12 +218,17 @@ async function prepare(ids, core, logs) {
|
|
|
111
218
|
}
|
|
112
219
|
|
|
113
220
|
/** One session per item, its output in `.gcf-session/runs/<id>.log`. */
|
|
114
|
-
async function session(r,
|
|
115
|
-
|
|
221
|
+
async function session(r, logs, timeoutMs) {
|
|
222
|
+
/** @type {import('node:fs/promises').FileHandle | null} */
|
|
223
|
+
let log = null;
|
|
116
224
|
try {
|
|
117
|
-
|
|
225
|
+
log = await open(path.join(logs, `${r.id}.log`), 'a');
|
|
226
|
+
|
|
227
|
+
return { ...r, code: await startAgent(r.agent, r.id, r.dir, { fd: log.fd, timeoutMs }) };
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { ...r, code: null, error: e.message };
|
|
118
230
|
} finally {
|
|
119
|
-
await log
|
|
231
|
+
await log?.close();
|
|
120
232
|
await r.release();
|
|
121
233
|
}
|
|
122
234
|
}
|
|
@@ -142,13 +254,15 @@ async function recordBlock(r, outcome, core) {
|
|
|
142
254
|
/** The result line of an item the integrator handled, or why it has none. */
|
|
143
255
|
function resultOf(run, integrated) {
|
|
144
256
|
const done = integrated.find((x) => x.id === run.id);
|
|
145
|
-
if (!done)
|
|
257
|
+
if (!done && run.error) return { id: run.id, result: 'error', error: run.error };
|
|
258
|
+
if (!done) {
|
|
259
|
+
const why = run.code === 'timeout' ? 'the session timed out' : `agent exit ${run.code}`;
|
|
260
|
+
|
|
146
261
|
return {
|
|
147
262
|
id: run.id,
|
|
148
|
-
result:
|
|
149
|
-
`no result (agent exit ${run.code}): ${run.id} stays ready, a rerun resumes its ` +
|
|
150
|
-
'worktree',
|
|
263
|
+
result: `no result (${why}): ${run.id} stays ready, a rerun resumes its worktree`,
|
|
151
264
|
};
|
|
265
|
+
}
|
|
152
266
|
|
|
153
267
|
return {
|
|
154
268
|
...done,
|
|
@@ -156,14 +270,22 @@ function resultOf(run, integrated) {
|
|
|
156
270
|
};
|
|
157
271
|
}
|
|
158
272
|
|
|
273
|
+
export const DEFAULT_TIMEOUT_MIN = 120;
|
|
274
|
+
|
|
159
275
|
/**
|
|
160
276
|
* Pick up orphans, gate the batch, run every item in its worktree at once, record each block
|
|
161
|
-
* on main, integrate the verified branches. `gate(ids)` answers `{pass, reason}
|
|
277
|
+
* on main, integrate the verified branches. `gate(ids)` answers `{pass, reason}`; a session
|
|
278
|
+
* past `timeoutMs` is stopped with all it started, and its item stays ready.
|
|
162
279
|
*/
|
|
163
280
|
export async function run(
|
|
164
281
|
projectDir,
|
|
165
282
|
ids,
|
|
166
|
-
{
|
|
283
|
+
{
|
|
284
|
+
core = cliCore(projectDir),
|
|
285
|
+
agent = DEFAULT_AGENT,
|
|
286
|
+
gate,
|
|
287
|
+
timeoutMs = DEFAULT_TIMEOUT_MIN * 60_000,
|
|
288
|
+
},
|
|
167
289
|
) {
|
|
168
290
|
const orphans = json(await core(['integrate', '--json']), 'integrate');
|
|
169
291
|
const passed = gate(ids);
|
|
@@ -171,8 +293,14 @@ export async function run(
|
|
|
171
293
|
|
|
172
294
|
const logs = path.join(path.resolve(projectDir), '.gcf-session', 'runs');
|
|
173
295
|
await mkdir(logs, { recursive: true });
|
|
174
|
-
const
|
|
175
|
-
const
|
|
296
|
+
const onBoard = json(await core(['board', '--json']), 'board').items ?? [];
|
|
297
|
+
const agentOf = (id) =>
|
|
298
|
+
agentFor(
|
|
299
|
+
agent,
|
|
300
|
+
onBoard.find((i) => i.id === id),
|
|
301
|
+
);
|
|
302
|
+
const { ready, failed } = await prepare(ids, core, logs, agentOf);
|
|
303
|
+
const runs = await Promise.all(ready.map((r) => session(r, logs, timeoutMs)));
|
|
176
304
|
const items = [...failed];
|
|
177
305
|
for (const r of runs) {
|
|
178
306
|
const outcome = await outcomeOf(r.dir);
|
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.
|
|
4
|
+
"version": "0.7.0",
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
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.
|
|
31
|
+
"gcf-aif-ext": "^0.6.0"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"extension.json",
|