@crouton-kit/crouter 0.3.2 → 0.3.8
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 +2 -2
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +3 -3
- package/dist/cli.js +6 -6
- package/dist/commands/__tests__/skill.test.js +24 -28
- package/dist/commands/{flow.d.ts → agent.d.ts} +1 -1
- package/dist/commands/agent.js +384 -0
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +7 -7
- package/dist/commands/human.js +6 -24
- package/dist/commands/job.js +54 -379
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +11 -11
- package/dist/commands/skill.js +114 -107
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +11 -11
- package/dist/core/__tests__/job.test.js +38 -74
- package/dist/core/__tests__/jobs.test.d.ts +1 -0
- package/dist/core/__tests__/jobs.test.js +66 -0
- package/dist/core/__tests__/resolver.test.d.ts +1 -0
- package/dist/core/__tests__/resolver.test.js +113 -0
- package/dist/core/config.js +20 -2
- package/dist/core/jobs.d.ts +26 -12
- package/dist/core/jobs.js +151 -42
- package/dist/core/resolver.d.ts +1 -2
- package/dist/core/resolver.js +60 -46
- package/dist/core/spawn.d.ts +26 -3
- package/dist/core/spawn.js +144 -11
- package/dist/prompts/agent.d.ts +3 -3
- package/dist/prompts/agent.js +20 -18
- package/dist/prompts/debug.js +14 -7
- package/dist/prompts/skill.js +16 -16
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/package.json +2 -2
- package/dist/commands/flow.js +0 -24
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
// Tests for the job subtree argv
|
|
2
|
-
// Exercises parseArgv against each leaf's param schema directly —
|
|
3
|
-
// no tmux, no filesystem side-effects from the handler.
|
|
1
|
+
// Tests for the job subtree (monitoring) and agent new * (spawning) argv
|
|
2
|
+
// migration. Exercises parseArgv against each leaf's param schema directly —
|
|
3
|
+
// no subprocess, no tmux, no filesystem side-effects from the handler.
|
|
4
4
|
//
|
|
5
5
|
// Run with: node --import tsx/esm --test src/core/__tests__/job.test.ts
|
|
6
6
|
import { test, describe } from 'node:test';
|
|
7
7
|
import assert from 'node:assert/strict';
|
|
8
|
-
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
8
|
import { parseArgv } from '../command.js';
|
|
12
9
|
// ---------------------------------------------------------------------------
|
|
13
10
|
// Param schemas extracted from each leaf (mirrors job.ts exactly)
|
|
@@ -51,9 +48,16 @@ const readLogsParams = [
|
|
|
51
48
|
{ kind: 'flag', name: 'level', type: 'enum', choices: ['debug', 'info', 'warn', 'error'], required: false, default: 'info', constraint: '' },
|
|
52
49
|
{ kind: 'flag', name: 'follow', type: 'bool', required: false, constraint: '' },
|
|
53
50
|
];
|
|
51
|
+
// NOTE: the real leaf also declares a `stdin` param for `body`. We omit it
|
|
52
|
+
// from the test schema because parseArgv reads stdin to EOF whenever a stdin
|
|
53
|
+
// param is declared — and under `node --test`, stdin is piped with no EOF, so
|
|
54
|
+
// the call hangs forever. The body-required-on-status=done check lives in the
|
|
55
|
+
// leaf's `run` handler, not in parseArgv, so the schema tests below cover
|
|
56
|
+
// everything parseArgv can see without needing stdin.
|
|
54
57
|
const submitParams = [
|
|
55
58
|
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
56
|
-
{ kind: '
|
|
59
|
+
{ kind: 'flag', name: 'status', type: 'enum', choices: ['done', 'failed'], required: false, default: 'done', constraint: '' },
|
|
60
|
+
{ kind: 'flag', name: 'reason', type: 'string', required: false, constraint: '' },
|
|
57
61
|
{ kind: 'flag', name: 'kill-pane', type: 'bool', required: false, constraint: '' },
|
|
58
62
|
];
|
|
59
63
|
const cancelParams = [
|
|
@@ -63,27 +67,9 @@ const failParams = [
|
|
|
63
67
|
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
64
68
|
];
|
|
65
69
|
// ---------------------------------------------------------------------------
|
|
66
|
-
//
|
|
70
|
+
// agent new prompt (formerly job start prompt)
|
|
67
71
|
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
function setup() {
|
|
70
|
-
tmpDir = join(tmpdir(), `crtr-job-test-${Date.now()}`);
|
|
71
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
72
|
-
}
|
|
73
|
-
function teardown() {
|
|
74
|
-
if (tmpDir !== undefined) {
|
|
75
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function writeTmpJson(name, obj) {
|
|
79
|
-
const p = join(tmpDir, name);
|
|
80
|
-
writeFileSync(p, JSON.stringify(obj), 'utf8');
|
|
81
|
-
return p;
|
|
82
|
-
}
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// job start prompt
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
describe('job start prompt', () => {
|
|
72
|
+
describe('agent new prompt', () => {
|
|
87
73
|
// stdin is handled by readStdinRaw() which requires actual stdin — we only
|
|
88
74
|
// test the non-stdin flag parsing here.
|
|
89
75
|
test('--cwd flag parsed as string', async () => {
|
|
@@ -105,9 +91,9 @@ describe('job start prompt', () => {
|
|
|
105
91
|
});
|
|
106
92
|
});
|
|
107
93
|
// ---------------------------------------------------------------------------
|
|
108
|
-
// job start fork
|
|
94
|
+
// agent new fork (formerly job start fork)
|
|
109
95
|
// ---------------------------------------------------------------------------
|
|
110
|
-
describe('
|
|
96
|
+
describe('agent new fork', () => {
|
|
111
97
|
test('no args parses cleanly', async () => {
|
|
112
98
|
const result = await parseArgv(startForkParams, []);
|
|
113
99
|
assert.equal(result['cwd'], undefined);
|
|
@@ -121,9 +107,9 @@ describe('job start fork', () => {
|
|
|
121
107
|
});
|
|
122
108
|
});
|
|
123
109
|
// ---------------------------------------------------------------------------
|
|
124
|
-
// job start planner
|
|
110
|
+
// agent new planner (formerly job start planner)
|
|
125
111
|
// ---------------------------------------------------------------------------
|
|
126
|
-
describe('
|
|
112
|
+
describe('agent new planner', () => {
|
|
127
113
|
test('positional spec_path required', async () => {
|
|
128
114
|
await assert.rejects(() => parseArgv(startPlannerParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
129
115
|
});
|
|
@@ -139,9 +125,9 @@ describe('job start planner', () => {
|
|
|
139
125
|
});
|
|
140
126
|
});
|
|
141
127
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// job start implementer
|
|
128
|
+
// agent new implementer (formerly job start implementer)
|
|
143
129
|
// ---------------------------------------------------------------------------
|
|
144
|
-
describe('
|
|
130
|
+
describe('agent new implementer', () => {
|
|
145
131
|
test('positional plan_path required', async () => {
|
|
146
132
|
await assert.rejects(() => parseArgv(startImplementerParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
147
133
|
});
|
|
@@ -151,9 +137,9 @@ describe('job start implementer', () => {
|
|
|
151
137
|
});
|
|
152
138
|
});
|
|
153
139
|
// ---------------------------------------------------------------------------
|
|
154
|
-
// job start reviewer
|
|
140
|
+
// agent new reviewer (formerly job start reviewer)
|
|
155
141
|
// ---------------------------------------------------------------------------
|
|
156
|
-
describe('
|
|
142
|
+
describe('agent new reviewer', () => {
|
|
157
143
|
test('positional + --kind required', async () => {
|
|
158
144
|
await assert.rejects(() => parseArgv(startReviewerParams, ['/tmp/artifact.md']), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
159
145
|
});
|
|
@@ -273,51 +259,29 @@ describe('job read logs', () => {
|
|
|
273
259
|
// job submit
|
|
274
260
|
// ---------------------------------------------------------------------------
|
|
275
261
|
describe('job submit', () => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
262
|
+
test('positional job_id + defaults: status=done, killPane=false', async () => {
|
|
263
|
+
const result = await parseArgv(submitParams, ['job-abc']);
|
|
264
|
+
assert.equal(result['job_id'], 'job-abc');
|
|
265
|
+
assert.equal(result['status'], 'done');
|
|
266
|
+
assert.equal(result['killPane'], false);
|
|
267
|
+
});
|
|
268
|
+
test('--status failed parsed', async () => {
|
|
269
|
+
const result = await parseArgv(submitParams, ['job-abc', '--status', 'failed', '--reason', 'broken']);
|
|
270
|
+
assert.equal(result['status'], 'failed');
|
|
271
|
+
assert.equal(result['reason'], 'broken');
|
|
272
|
+
});
|
|
273
|
+
test('--status invalid enum throws', async () => {
|
|
274
|
+
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--status', 'bogus']), (err) => { assert.match(err.message, /must be one of/); return true; });
|
|
290
275
|
});
|
|
291
276
|
test('--kill-pane presence = true, killPane key', async () => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const p = writeTmpJson('result.json', { status: 'done' });
|
|
295
|
-
const result = await parseArgv(submitParams, ['job-abc', '--context-file', p, '--kill-pane']);
|
|
296
|
-
assert.equal(result['killPane'], true);
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
teardown();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
test('missing --context-file throws missing_parameter', async () => {
|
|
303
|
-
await assert.rejects(() => parseArgv(submitParams, ['job-abc']), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
277
|
+
const result = await parseArgv(submitParams, ['job-abc', '--kill-pane']);
|
|
278
|
+
assert.equal(result['killPane'], true);
|
|
304
279
|
});
|
|
305
280
|
test('missing positional job_id throws missing_parameter', async () => {
|
|
306
281
|
await assert.rejects(() => parseArgv(submitParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
307
282
|
});
|
|
308
|
-
test('--context-file
|
|
309
|
-
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--context-file', '/
|
|
310
|
-
});
|
|
311
|
-
test('--context-file with invalid JSON throws invalid_type', async () => {
|
|
312
|
-
setup();
|
|
313
|
-
try {
|
|
314
|
-
const p = join(tmpDir, 'bad.json');
|
|
315
|
-
writeFileSync(p, 'not json', 'utf8');
|
|
316
|
-
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--context-file', p]), (err) => { assert.match(err.message, /not valid JSON/); return true; });
|
|
317
|
-
}
|
|
318
|
-
finally {
|
|
319
|
-
teardown();
|
|
320
|
-
}
|
|
283
|
+
test('--context-file no longer accepted', async () => {
|
|
284
|
+
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--context-file', '/tmp/anything']), (err) => { assert.match(err.message, /unknown flag: --context-file/); return true; });
|
|
321
285
|
});
|
|
322
286
|
});
|
|
323
287
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Tests for jobs.ts result-file storage (markdown vs json paths).
|
|
2
|
+
//
|
|
3
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/jobs.test.ts
|
|
4
|
+
import { test, describe, before, after } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createJob, writeResult, writeMarkdownResult, readResult, } from '../jobs.js';
|
|
10
|
+
let stateDir;
|
|
11
|
+
let origXdg;
|
|
12
|
+
before(() => {
|
|
13
|
+
stateDir = join(tmpdir(), `crtr-jobs-test-${Date.now()}`);
|
|
14
|
+
mkdirSync(stateDir, { recursive: true });
|
|
15
|
+
origXdg = process.env['XDG_STATE_HOME'];
|
|
16
|
+
process.env['XDG_STATE_HOME'] = stateDir;
|
|
17
|
+
});
|
|
18
|
+
after(() => {
|
|
19
|
+
if (origXdg === undefined) {
|
|
20
|
+
delete process.env['XDG_STATE_HOME'];
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
process.env['XDG_STATE_HOME'] = origXdg;
|
|
24
|
+
}
|
|
25
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
describe('writeMarkdownResult + readResult round-trip', () => {
|
|
28
|
+
test('done with body writes result.md, parses frontmatter back', async () => {
|
|
29
|
+
const { jobId, dir } = createJob('prompt', { cwd: '/tmp' });
|
|
30
|
+
const body = '**Summary:** all good\n\nMore details on the next line.\n';
|
|
31
|
+
writeMarkdownResult(jobId, body, 'done');
|
|
32
|
+
assert.ok(existsSync(join(dir, 'result.md')), 'result.md should exist');
|
|
33
|
+
assert.ok(!existsSync(join(dir, 'result.json')), 'result.json should NOT exist on md path');
|
|
34
|
+
const raw = readFileSync(join(dir, 'result.md'), 'utf8');
|
|
35
|
+
assert.match(raw, /^---\nstatus: done\nwritten_at: \d{4}-\d{2}-\d{2}T/);
|
|
36
|
+
assert.ok(raw.endsWith(body), 'body preserved at end of file');
|
|
37
|
+
const r = await readResult(jobId, { waitMs: 0 });
|
|
38
|
+
assert.equal(r.status, 'done');
|
|
39
|
+
assert.equal(r.result_md, body);
|
|
40
|
+
assert.equal(r.reason, undefined);
|
|
41
|
+
assert.equal(r.result, undefined);
|
|
42
|
+
});
|
|
43
|
+
test('failed with reason writes reason into frontmatter and reads it back', async () => {
|
|
44
|
+
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
45
|
+
writeMarkdownResult(jobId, '', 'failed', 'broke: had "quoted" parts and a\nnewline');
|
|
46
|
+
const r = await readResult(jobId, { waitMs: 0 });
|
|
47
|
+
assert.equal(r.status, 'failed');
|
|
48
|
+
assert.equal(r.result_md, '');
|
|
49
|
+
assert.equal(r.reason, 'broke: had "quoted" parts and a\nnewline');
|
|
50
|
+
});
|
|
51
|
+
test('writeResult (JSON) writes result.json and read still works', async () => {
|
|
52
|
+
const { jobId, dir } = createJob('prompt', { cwd: '/tmp' });
|
|
53
|
+
writeResult(jobId, { feedback: 'approved', n: 3 }, 'done');
|
|
54
|
+
assert.ok(existsSync(join(dir, 'result.json')));
|
|
55
|
+
assert.ok(!existsSync(join(dir, 'result.md')));
|
|
56
|
+
const r = await readResult(jobId, { waitMs: 0 });
|
|
57
|
+
assert.equal(r.status, 'done');
|
|
58
|
+
assert.deepEqual(r.result, { feedback: 'approved', n: 3 });
|
|
59
|
+
assert.equal(r.result_md, undefined);
|
|
60
|
+
});
|
|
61
|
+
test('readResult with no result file and waitMs=0 returns timeout', async () => {
|
|
62
|
+
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
63
|
+
const r = await readResult(jobId, { waitMs: 0 });
|
|
64
|
+
assert.equal(r.status, 'timeout');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Tests for parseSkillQualifier (slash-only form) and config migration.
|
|
2
|
+
//
|
|
3
|
+
// Run with: node --import tsx/esm --test 'src/core/__tests__/resolver.test.ts'
|
|
4
|
+
import { test, describe, before, after } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { parseSkillQualifier } from '../resolver.js';
|
|
10
|
+
import { InputError } from '../io.js';
|
|
11
|
+
import { readConfig } from '../config.js';
|
|
12
|
+
import { SCHEMA_VERSION } from '../../types.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// parseSkillQualifier — slash-only form
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
describe('parseSkillQualifier', () => {
|
|
17
|
+
test('bare name returns single segment', () => {
|
|
18
|
+
const r = parseSkillQualifier('foo');
|
|
19
|
+
assert.deepEqual(r, { segments: ['foo'] });
|
|
20
|
+
});
|
|
21
|
+
test('plugin/name returns two segments', () => {
|
|
22
|
+
const r = parseSkillQualifier('ai/interface');
|
|
23
|
+
assert.deepEqual(r, { segments: ['ai', 'interface'] });
|
|
24
|
+
});
|
|
25
|
+
test('plugin/nested/name returns three segments', () => {
|
|
26
|
+
const r = parseSkillQualifier('ai/interface/cli-design');
|
|
27
|
+
assert.deepEqual(r, { segments: ['ai', 'interface', 'cli-design'] });
|
|
28
|
+
});
|
|
29
|
+
test('scope-qualified user/name sets scope, single segment', () => {
|
|
30
|
+
const r = parseSkillQualifier('user/my-skill');
|
|
31
|
+
assert.deepEqual(r, { scope: 'user', segments: ['my-skill'] });
|
|
32
|
+
});
|
|
33
|
+
test('scope-qualified project/name sets scope', () => {
|
|
34
|
+
const r = parseSkillQualifier('project/my-skill');
|
|
35
|
+
assert.deepEqual(r, { scope: 'project', segments: ['my-skill'] });
|
|
36
|
+
});
|
|
37
|
+
test('scope-qualified user/ai/x/y sets scope and leaves rest as segments', () => {
|
|
38
|
+
const r = parseSkillQualifier('user/ai/x/y');
|
|
39
|
+
assert.deepEqual(r, { scope: 'user', segments: ['ai', 'x', 'y'] });
|
|
40
|
+
});
|
|
41
|
+
test('colon in input throws InputError with invalid_qualifier', () => {
|
|
42
|
+
assert.throws(() => parseSkillQualifier('ai:interface/cli-design'), (e) => {
|
|
43
|
+
assert.ok(e instanceof InputError, 'should be InputError');
|
|
44
|
+
assert.equal(e.payload.error, 'invalid_qualifier');
|
|
45
|
+
assert.equal(e.payload.received, 'ai:interface/cli-design');
|
|
46
|
+
assert.ok(e.payload.next.includes('ai/interface/cli-design'), `next should contain slash form, got: ${e.payload.next}`);
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
test('legacy scope:name form throws invalid_qualifier', () => {
|
|
51
|
+
assert.throws(() => parseSkillQualifier('user:my-skill'), (e) => {
|
|
52
|
+
assert.ok(e instanceof InputError);
|
|
53
|
+
assert.equal(e.payload.error, 'invalid_qualifier');
|
|
54
|
+
assert.ok(e.payload.next.includes('user/my-skill'));
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
test('legacy scope:plugin/name form throws invalid_qualifier', () => {
|
|
59
|
+
assert.throws(() => parseSkillQualifier('user:ai/interface/cli-design'), (e) => {
|
|
60
|
+
assert.ok(e instanceof InputError);
|
|
61
|
+
assert.equal(e.payload.error, 'invalid_qualifier');
|
|
62
|
+
assert.ok(e.payload.next.includes('user/ai/interface/cli-design'));
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Config migration: colon keys → slash keys on readConfig with old schema_version
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
describe('config migration: skill keys colon → slash', () => {
|
|
71
|
+
let testHomeDir;
|
|
72
|
+
let origHome;
|
|
73
|
+
let crouterDir;
|
|
74
|
+
before(() => {
|
|
75
|
+
testHomeDir = join(tmpdir(), `crtr-resolver-test-${Date.now()}`);
|
|
76
|
+
mkdirSync(testHomeDir, { recursive: true });
|
|
77
|
+
origHome = process.env['HOME'];
|
|
78
|
+
process.env['HOME'] = testHomeDir;
|
|
79
|
+
crouterDir = join(testHomeDir, '.crouter');
|
|
80
|
+
mkdirSync(crouterDir, { recursive: true });
|
|
81
|
+
});
|
|
82
|
+
after(() => {
|
|
83
|
+
if (origHome === undefined) {
|
|
84
|
+
delete process.env['HOME'];
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
process.env['HOME'] = origHome;
|
|
88
|
+
}
|
|
89
|
+
rmSync(testHomeDir, { recursive: true, force: true });
|
|
90
|
+
});
|
|
91
|
+
test('colon keys are migrated to slash keys and written with new schema_version', () => {
|
|
92
|
+
const oldConfig = {
|
|
93
|
+
schema_version: 1,
|
|
94
|
+
marketplaces: {},
|
|
95
|
+
plugins: {},
|
|
96
|
+
skills: {
|
|
97
|
+
'ai:foo': { enabled: true },
|
|
98
|
+
'ai:interface/cli-design': { enabled: false },
|
|
99
|
+
},
|
|
100
|
+
auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
|
|
101
|
+
max_panes_per_window: 3,
|
|
102
|
+
};
|
|
103
|
+
writeFileSync(join(crouterDir, 'config.json'), JSON.stringify(oldConfig), 'utf8');
|
|
104
|
+
const cfg = readConfig('user');
|
|
105
|
+
assert.ok(!('ai:foo' in cfg.skills), 'old colon key should be gone');
|
|
106
|
+
assert.ok('ai/foo' in cfg.skills, 'slash key should exist');
|
|
107
|
+
assert.equal(cfg.skills['ai/foo']?.enabled, true, 'enabled state preserved');
|
|
108
|
+
assert.ok(!('ai:interface/cli-design' in cfg.skills), 'old colon key should be gone');
|
|
109
|
+
assert.ok('ai/interface/cli-design' in cfg.skills, 'slash key should exist for nested');
|
|
110
|
+
assert.equal(cfg.skills['ai/interface/cli-design']?.enabled, false, 'disabled state preserved');
|
|
111
|
+
assert.equal(cfg.schema_version, SCHEMA_VERSION, 'schema_version bumped to current');
|
|
112
|
+
});
|
|
113
|
+
});
|
package/dist/core/config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState } from '../types.js';
|
|
2
|
+
import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState } from '../types.js';
|
|
3
3
|
import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
4
|
import { scopeRoot, requireScopeRoot } from './scope.js';
|
|
5
|
+
import { diag } from './io.js';
|
|
5
6
|
function configPathFor(root) {
|
|
6
7
|
return join(root, CONFIG_FILE);
|
|
7
8
|
}
|
|
@@ -23,7 +24,24 @@ export function readConfig(scope) {
|
|
|
23
24
|
const existing = readJsonIfExists(configPathFor(root));
|
|
24
25
|
if (!existing)
|
|
25
26
|
return defaultScopeConfig();
|
|
26
|
-
|
|
27
|
+
const cfg = mergeConfig(existing);
|
|
28
|
+
if ((existing.schema_version ?? 0) < SCHEMA_VERSION) {
|
|
29
|
+
migrateSkillConfigKeys(cfg, scope, root);
|
|
30
|
+
}
|
|
31
|
+
return cfg;
|
|
32
|
+
}
|
|
33
|
+
function migrateSkillConfigKeys(cfg, scope, root) {
|
|
34
|
+
const colonKeys = Object.keys(cfg.skills).filter((k) => k.includes(':'));
|
|
35
|
+
if (colonKeys.length > 0) {
|
|
36
|
+
for (const key of colonKeys) {
|
|
37
|
+
const newKey = key.replace(/:/g, '/');
|
|
38
|
+
cfg.skills[newKey] = cfg.skills[key];
|
|
39
|
+
delete cfg.skills[key];
|
|
40
|
+
}
|
|
41
|
+
diag(`crtr: migrated ${colonKeys.length} skill config keys to slash form in ${scope}`);
|
|
42
|
+
}
|
|
43
|
+
cfg.schema_version = SCHEMA_VERSION;
|
|
44
|
+
writeJson(configPathFor(root), cfg);
|
|
27
45
|
}
|
|
28
46
|
export function readState(scope) {
|
|
29
47
|
const root = scopeRoot(scope);
|
package/dist/core/jobs.d.ts
CHANGED
|
@@ -27,28 +27,42 @@ export declare function appendEvent(jobId: string, event: {
|
|
|
27
27
|
data?: object;
|
|
28
28
|
}): void;
|
|
29
29
|
/**
|
|
30
|
-
* Atomically write result.json and update meta.json status.
|
|
31
|
-
*
|
|
32
|
-
* log content.
|
|
30
|
+
* Atomically write result.json (structured object) and update meta.json status.
|
|
31
|
+
* Used by programmatic callers (human, sys) that produce object results.
|
|
32
|
+
* The result file's appearance is the completion signal — never inferred from log content.
|
|
33
33
|
*/
|
|
34
34
|
export declare function writeResult(jobId: string, result: object, terminalStatus: TerminalStatus): void;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Atomically write result.md (YAML frontmatter + markdown body) and update meta.json status.
|
|
37
|
+
* Used by `crtr job submit` for agent-driven markdown results.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeMarkdownResult(jobId: string, body: string, terminalStatus: TerminalStatus, reason?: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Read whichever result file exists (result.md or result.json). If neither
|
|
42
|
+
* exists and waitMs is given, block via fs.watch until one appears or the
|
|
43
|
+
* timeout elapses.
|
|
38
44
|
*
|
|
39
|
-
* Race safety: registers the watcher THEN re-stats. If result
|
|
45
|
+
* Race safety: registers the watcher THEN re-stats. If a result file appeared
|
|
40
46
|
* between the first stat and the watch registration, the re-stat catches it
|
|
41
47
|
* before the watcher has a chance to miss it.
|
|
48
|
+
*
|
|
49
|
+
* Returns shape:
|
|
50
|
+
* - JSON path: { status, result: object }
|
|
51
|
+
* - Markdown path: { status, result_md: string, reason?: string }
|
|
52
|
+
* - Timeout: { status: 'timeout' }
|
|
42
53
|
*/
|
|
43
|
-
export
|
|
44
|
-
waitMs?: number;
|
|
45
|
-
}): Promise<{
|
|
54
|
+
export interface ReadResultResponse {
|
|
46
55
|
status: 'done' | 'failed' | 'canceled' | 'timeout';
|
|
47
56
|
result?: object;
|
|
48
|
-
|
|
57
|
+
result_md?: string;
|
|
58
|
+
reason?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function readResult(jobId: string, opts?: {
|
|
61
|
+
waitMs?: number;
|
|
62
|
+
}): Promise<ReadResultResponse>;
|
|
49
63
|
/**
|
|
50
|
-
* Derive job state from meta.json, result
|
|
51
|
-
* If a pid is recorded, is not alive, and no result
|
|
64
|
+
* Derive job state from meta.json, the result file, and the tail of log.jsonl.
|
|
65
|
+
* If a pid is recorded, is not alive, and no result file exists → 'failed'.
|
|
52
66
|
*/
|
|
53
67
|
export declare function jobStatus(jobId: string): {
|
|
54
68
|
state: JobState;
|