cowork-flow 1.4.0 → 1.6.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/CHANGELOG.md +81 -1
- package/README.md +85 -9
- package/package.json +2 -2
- package/presets/dsh/plugins/workflow-state.js +152 -0
- package/presets/kimi-code/hooks/cowork-flow-inject.mjs +113 -0
- package/src/cli.js +6 -0
- package/src/commands/install-dsh-preset.js +36 -2
- package/src/commands/install-kimi-hook.js +343 -0
- package/src/lib/host-assets.js +1 -10
- package/template/.codex/hooks.json +12 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/kimi-code/adapter.yaml +31 -0
- package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/state.py +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +38 -23
- package/template/.cowork-flow/scripts/adapters/cli/task.py +0 -3
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +5 -2
- package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +3 -14
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +1 -2
- package/template/.cowork-flow/scripts/adapters/host/inject.py +14 -8
- package/template/.cowork-flow/scripts/adapters/host/kimi_code_policy.py +21 -0
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +61 -20
- package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +0 -17
- package/template/.cowork-flow/scripts/adapters/mcp/state_server.py +1 -1
- package/template/.cowork-flow/scripts/adapters/review/test_intent.py +0 -31
- package/template/.cowork-flow/scripts/infra/paths.py +0 -8
- package/template/.cowork-flow/scripts/run.py +1 -0
- package/template/.cowork-flow/scripts/runtime/host_identity.py +260 -0
- package/template/.cowork-flow/scripts/runtime/session_state.py +48 -53
- package/template/.cowork-flow/scripts/services/context_discovery.py +0 -35
- package/template/.cowork-flow/scripts/services/fact_view.py +1 -2
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +0 -20
- package/template/.cowork-flow/scripts/services/spec_check.py +1 -1
- package/template/.cowork-flow/scripts/services/task_context.py +0 -2
- package/template/.cowork-flow/spec/contracts/context-injection.md +91 -5
- package/template/.cowork-flow/spec/contracts/fact-layer-access.md +2 -1
- package/template/.cowork-flow/spec/contracts/spec-checks.md +16 -5
- package/template/.cowork-flow/spec/runtime/host-assets.json +49 -0
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +1 -8
- package/template/.cowork-flow/spec/schemas/index.md +1 -0
- package/template/.kimi-code/agents/cowork-check.md +29 -0
- package/template/.kimi-code/agents/cowork-implement.md +30 -0
- package/template/.kimi-code/agents/cowork-research.md +23 -0
- package/template/.opencode/plugins/cowork-flow.js +86 -0
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/AGENTS.md +1 -0
- package/template/skills/runtime-health/scripts/doctor.py +186 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { cp, mkdir, rm, access } from 'node:fs/promises';
|
|
1
|
+
import { cp, mkdir, rm, access, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
|
|
5
5
|
import { packageRoot, templateRoot } from '../lib/paths.js';
|
|
6
|
+
import { readPackageInfo } from '../lib/package-info.js';
|
|
6
7
|
|
|
7
8
|
const PRESET_ID = 'cowork-flow';
|
|
9
|
+
const MARKER_FILE = '.cowork-flow-preset.json';
|
|
8
10
|
const PRESET_SRC = join(packageRoot, 'presets', 'dsh');
|
|
9
11
|
const SKILLS_SRC = join(templateRoot, 'skills');
|
|
10
12
|
|
|
@@ -33,6 +35,17 @@ function getDshPresetRoot() {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
async function readInstalledVersion(destDir) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readFile(join(destDir, MARKER_FILE), 'utf8');
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
export async function runInstallDshPreset(args = []) {
|
|
37
50
|
const { dryRun, force } = parseArgs(args);
|
|
38
51
|
|
|
@@ -53,9 +66,25 @@ export async function runInstallDshPreset(args = []) {
|
|
|
53
66
|
return;
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
const { version } = await readPackageInfo();
|
|
70
|
+
|
|
56
71
|
if (!force && (await pathExists(destDir))) {
|
|
72
|
+
const installedVersion = await readInstalledVersion(destDir);
|
|
57
73
|
console.log(`cowork-flow DSH preset already installed at ${destDir}`);
|
|
58
|
-
|
|
74
|
+
if (installedVersion === null) {
|
|
75
|
+
console.log(
|
|
76
|
+
`Installed version unknown (no ${MARKER_FILE}); current version is ${version}.`
|
|
77
|
+
);
|
|
78
|
+
console.log('Refresh it with: cowork-flow install-dsh-preset --force');
|
|
79
|
+
} else if (installedVersion !== version) {
|
|
80
|
+
console.log(
|
|
81
|
+
`Installed version ${installedVersion} differs from current version ${version}.`
|
|
82
|
+
);
|
|
83
|
+
console.log('The preset does not update with sync or npm; refresh it with:');
|
|
84
|
+
console.log(' cowork-flow install-dsh-preset --force');
|
|
85
|
+
} else {
|
|
86
|
+
console.log('Use --force to overwrite.');
|
|
87
|
+
}
|
|
59
88
|
return;
|
|
60
89
|
}
|
|
61
90
|
|
|
@@ -66,6 +95,11 @@ export async function runInstallDshPreset(args = []) {
|
|
|
66
95
|
|
|
67
96
|
await cp(PRESET_SRC, destDir, { recursive: true });
|
|
68
97
|
await cp(SKILLS_SRC, skillsDest, { recursive: true, force: true });
|
|
98
|
+
await writeFile(
|
|
99
|
+
join(destDir, MARKER_FILE),
|
|
100
|
+
`${JSON.stringify({ version, installedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
101
|
+
'utf8'
|
|
102
|
+
);
|
|
69
103
|
|
|
70
104
|
console.log(`✓ cowork-flow DSH preset installed to ${destDir}`);
|
|
71
105
|
console.log(' Start a new DSH session and pick the "Cowork Flow" preset.');
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import { packageRoot } from '../lib/paths.js';
|
|
6
|
+
import { readPackageInfo } from '../lib/package-info.js';
|
|
7
|
+
|
|
8
|
+
const SHIM_NAME = 'cowork-flow-inject.mjs';
|
|
9
|
+
const SHIM_SRC = join(packageRoot, 'presets', 'kimi-code', 'hooks', SHIM_NAME);
|
|
10
|
+
const MARKER_FILE = '.cowork-flow-kimi-hook.json';
|
|
11
|
+
const HOOK_EVENT = 'UserPromptSubmit';
|
|
12
|
+
// Kimi Code's own hook default; kept explicit so an edit is visible here.
|
|
13
|
+
const HOOK_TIMEOUT = 30;
|
|
14
|
+
const MANAGED_START = '# cowork-flow: kimi hook start. Managed by "cowork-flow install-kimi-hook"; edits inside this block are replaced.';
|
|
15
|
+
const MANAGED_END = '# cowork-flow: kimi hook end.';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function parseArgs(args) {
|
|
19
|
+
return {
|
|
20
|
+
dryRun: args.includes('--dry-run'),
|
|
21
|
+
force: args.includes('--force'),
|
|
22
|
+
uninstall: args.includes('--uninstall')
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function getKimiHome() {
|
|
28
|
+
return process.env.KIMI_CODE_HOME || join(homedir(), '.kimi-code');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async function pathExists(target) {
|
|
33
|
+
try {
|
|
34
|
+
await access(target);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async function readIfExists(file) {
|
|
43
|
+
try {
|
|
44
|
+
return await readFile(file, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Kimi Code's user-level config.toml, holding the `[[hooks]]` rows.
|
|
53
|
+
*/
|
|
54
|
+
function configFile(home) {
|
|
55
|
+
return join(home, 'config.toml');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transport shim installed beside the config, spawned as
|
|
61
|
+
* `node <home>/hooks/cowork-flow-inject.mjs` from every session.
|
|
62
|
+
*/
|
|
63
|
+
function shimDest(home) {
|
|
64
|
+
return join(home, 'hooks', SHIM_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Version marker for the installed hook, written next to the shim. The
|
|
70
|
+
* installed files live outside any project, so nothing else records which
|
|
71
|
+
* release put them there — doctor reads this to tell an up-to-date install
|
|
72
|
+
* from one that predates the project runtime.
|
|
73
|
+
*/
|
|
74
|
+
function markerDest(home) {
|
|
75
|
+
return join(home, 'hooks', MARKER_FILE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One `[[hooks]]` row that pipes every UserPromptSubmit payload into the
|
|
81
|
+
* shim. Kimi Code allows exactly four hook fields — event, matcher, command,
|
|
82
|
+
* timeout — and rejects the whole config when a row carries anything else,
|
|
83
|
+
* so the row is deliberately minimal. `matcher` is omitted on purpose:
|
|
84
|
+
* omitted means "match every prompt". Paths are quoted because a Kimi Code
|
|
85
|
+
* home may contain spaces (Windows user profiles).
|
|
86
|
+
*/
|
|
87
|
+
function hookBlock(home) {
|
|
88
|
+
const command = 'node "' + shimDest(home) + '"';
|
|
89
|
+
return [
|
|
90
|
+
MANAGED_START,
|
|
91
|
+
'[[hooks]]',
|
|
92
|
+
'event = ' + JSON.stringify(HOOK_EVENT),
|
|
93
|
+
'command = ' + JSON.stringify(command),
|
|
94
|
+
'timeout = ' + HOOK_TIMEOUT,
|
|
95
|
+
MANAGED_END
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* End of the `[[hooks]]` row starting at `start`: the next table header, the
|
|
102
|
+
* next row of the same array, or the end of file. Trailing blank lines stay
|
|
103
|
+
* outside the range so removal leaves them for the surrounding layout.
|
|
104
|
+
*/
|
|
105
|
+
function hookRowEnd(lines, start) {
|
|
106
|
+
let end = start + 1;
|
|
107
|
+
while (end < lines.length) {
|
|
108
|
+
const trimmed = lines[end].trim();
|
|
109
|
+
if (trimmed === '[[hooks]]' || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
end += 1;
|
|
113
|
+
}
|
|
114
|
+
while (end > start + 1 && lines[end - 1].trim() === '') {
|
|
115
|
+
end -= 1;
|
|
116
|
+
}
|
|
117
|
+
return end;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Locate the managed block inside the config text.
|
|
123
|
+
*
|
|
124
|
+
* The config is edited as text on purpose: a TOML round-trip would rewrite
|
|
125
|
+
* every pretty-printed entry of the user's own config (providers, models,
|
|
126
|
+
* permissions), and the file must stay byte-stable outside the block. The
|
|
127
|
+
* marker comments take precedence so the block is removed even when the row
|
|
128
|
+
* no longer resembles ours; without markers, the only ownership signal left
|
|
129
|
+
* is a `[[hooks]]` row whose command names this shim. Returns null when
|
|
130
|
+
* neither exists.
|
|
131
|
+
*/
|
|
132
|
+
function findManagedBlock(text, home) {
|
|
133
|
+
const lines = text.split('\n');
|
|
134
|
+
const marked = lines.findIndex((line) => line.trim() === MANAGED_START);
|
|
135
|
+
if (marked !== -1) {
|
|
136
|
+
let end = marked + 1;
|
|
137
|
+
while (end < lines.length && lines[end].trim() !== MANAGED_END) {
|
|
138
|
+
end += 1;
|
|
139
|
+
}
|
|
140
|
+
return { start: marked, end: Math.min(end + 1, lines.length) };
|
|
141
|
+
}
|
|
142
|
+
const owns = shimDest(home).replace(/\\/g, '/');
|
|
143
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
144
|
+
if (lines[index].trim() !== '[[hooks]]') {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const end = hookRowEnd(lines, index);
|
|
148
|
+
const row = lines.slice(index, end).join('\n');
|
|
149
|
+
if (row.includes(MANAGED_START) || row.replace(/\\/g, '/').includes(owns)) {
|
|
150
|
+
return { start: index, end };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
function withBlock(text, home) {
|
|
158
|
+
const block = hookBlock(home).split('\n');
|
|
159
|
+
const range = findManagedBlock(text, home);
|
|
160
|
+
if (range !== null) {
|
|
161
|
+
const lines = text.split('\n');
|
|
162
|
+
if (lines.slice(range.start, range.end).join('\n') === block.join('\n')) {
|
|
163
|
+
return { text, changed: false, existed: true };
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
text: lines.slice(0, range.start).concat(block).concat(lines.slice(range.end)).join('\n'),
|
|
167
|
+
changed: true,
|
|
168
|
+
existed: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const base = text.replace(/\s+$/, '');
|
|
172
|
+
const appended = base === '' ? block.join('\n') : base + '\n\n' + block.join('\n');
|
|
173
|
+
return { text: appended + '\n', changed: true, existed: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
function withoutBlock(text, home) {
|
|
178
|
+
const range = findManagedBlock(text, home);
|
|
179
|
+
if (range === null) {
|
|
180
|
+
return { text, changed: false };
|
|
181
|
+
}
|
|
182
|
+
const lines = text.split('\n');
|
|
183
|
+
const kept = lines.slice(0, range.start).concat(lines.slice(range.end));
|
|
184
|
+
const body = kept.join('\n').replace(/^\n+/, '').replace(/\s+$/, '');
|
|
185
|
+
if (body === '') {
|
|
186
|
+
// Nothing of the user's own config remains: an empty file left behind
|
|
187
|
+
// would be a config that only ever held this hook.
|
|
188
|
+
return { text: '', changed: true };
|
|
189
|
+
}
|
|
190
|
+
return { text: body + '\n', changed: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compact line preview for --dry-run: every line that differs plus its
|
|
196
|
+
* neighbours, so the operator sees where the block lands without a full-file
|
|
197
|
+
* dump.
|
|
198
|
+
*/
|
|
199
|
+
function previewDiff(before, after) {
|
|
200
|
+
// A trailing newline is a line terminator, not an extra empty line; without
|
|
201
|
+
// this the preview would always report a phantom last-line change.
|
|
202
|
+
const toLines = (text) => {
|
|
203
|
+
if (text === '') {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
return (text.endsWith('\n') ? text.slice(0, -1) : text).split('\n');
|
|
207
|
+
};
|
|
208
|
+
const left = toLines(before);
|
|
209
|
+
const right = toLines(after);
|
|
210
|
+
const total = Math.max(left.length, right.length);
|
|
211
|
+
const shown = new Set();
|
|
212
|
+
for (let index = 0; index < total; index += 1) {
|
|
213
|
+
if (left[index] !== right[index]) {
|
|
214
|
+
for (let offset = -2; offset <= 2; offset += 1) {
|
|
215
|
+
if (index + offset >= 0 && index + offset < total) {
|
|
216
|
+
shown.add(index + offset);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const lines = [];
|
|
222
|
+
let previous = -1;
|
|
223
|
+
for (const index of [...shown].sort((a, b) => a - b)) {
|
|
224
|
+
if (previous !== -1 && index > previous + 1) {
|
|
225
|
+
lines.push(' ...');
|
|
226
|
+
}
|
|
227
|
+
const marker = left[index] === right[index] ? ' ' : '+ ';
|
|
228
|
+
lines.push(' ' + (index + 1) + ' ' + marker + (right[index] ?? ''));
|
|
229
|
+
previous = index;
|
|
230
|
+
}
|
|
231
|
+
return lines;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Install (or uninstall) the user-level Kimi Code context-injection hook.
|
|
237
|
+
*
|
|
238
|
+
* Kimi Code reads hooks only from $KIMI_CODE_HOME/config.toml, so the shim is
|
|
239
|
+
* installed under the same home and every session — in every project —
|
|
240
|
+
* invokes it; the shim itself exits silently outside cowork-flow projects.
|
|
241
|
+
* Only the Kimi Code home is written: no project file is touched.
|
|
242
|
+
*/
|
|
243
|
+
export async function runInstallKimiHook(args = []) {
|
|
244
|
+
const { dryRun, uninstall } = parseArgs(args);
|
|
245
|
+
|
|
246
|
+
if (!(await pathExists(SHIM_SRC))) {
|
|
247
|
+
throw new Error('Kimi Code hook shim missing at ' + SHIM_SRC + '. Reinstall cowork-flow.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const home = getKimiHome();
|
|
251
|
+
const config = configFile(home);
|
|
252
|
+
const shim = shimDest(home);
|
|
253
|
+
const marker = markerDest(home);
|
|
254
|
+
|
|
255
|
+
if (dryRun) {
|
|
256
|
+
const before = await readIfExists(config);
|
|
257
|
+
if (uninstall) {
|
|
258
|
+
const result = before === null ? { changed: false } : withoutBlock(before, home);
|
|
259
|
+
console.log('[dry-run] Would uninstall Kimi Code context hook:');
|
|
260
|
+
console.log(
|
|
261
|
+
' Config: ' + config
|
|
262
|
+
+ (before === null
|
|
263
|
+
? ' (absent; nothing to remove)'
|
|
264
|
+
: (result.changed ? ' (managed block found)' : ' (no managed block)'))
|
|
265
|
+
);
|
|
266
|
+
console.log(' Remove shim: ' + shim);
|
|
267
|
+
console.log(' Remove marker: ' + marker);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.log('[dry-run] Would install Kimi Code context hook:');
|
|
271
|
+
console.log(' Shim: ' + SHIM_SRC + ' -> ' + shim);
|
|
272
|
+
console.log(' Marker: ' + marker);
|
|
273
|
+
if (before === null) {
|
|
274
|
+
console.log(' Create config: ' + config);
|
|
275
|
+
for (const line of previewDiff('', hookBlock(home))) {
|
|
276
|
+
console.log(line);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const result = withBlock(before, home);
|
|
281
|
+
if (!result.changed) {
|
|
282
|
+
console.log(' Config: ' + config + ' (managed block already up to date)');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
console.log(' Config: ' + config + (result.existed ? ' (update managed block)' : ' (append managed block)'));
|
|
286
|
+
for (const line of previewDiff(before, result.text)) {
|
|
287
|
+
console.log(line);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (uninstall) {
|
|
293
|
+
const before = await readIfExists(config);
|
|
294
|
+
if (before === null) {
|
|
295
|
+
console.log('No Kimi Code config at ' + config + '; nothing to uninstall.');
|
|
296
|
+
} else {
|
|
297
|
+
const result = withoutBlock(before, home);
|
|
298
|
+
if (!result.changed) {
|
|
299
|
+
console.log('No managed hook block found in ' + config + '.');
|
|
300
|
+
} else if (result.text === '') {
|
|
301
|
+
await rm(config, { force: true });
|
|
302
|
+
console.log('✓ Kimi Code context hook removed; ' + config + ' held nothing else and was deleted.');
|
|
303
|
+
} else {
|
|
304
|
+
await writeFile(config, result.text, 'utf8');
|
|
305
|
+
console.log('✓ Kimi Code context hook removed from ' + config);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const hadShim = await pathExists(shim);
|
|
309
|
+
await rm(shim, { force: true });
|
|
310
|
+
if (hadShim) {
|
|
311
|
+
console.log('✓ Removed hook shim ' + shim);
|
|
312
|
+
}
|
|
313
|
+
const hadMarker = await pathExists(marker);
|
|
314
|
+
await rm(marker, { force: true });
|
|
315
|
+
if (hadMarker) {
|
|
316
|
+
console.log('✓ Removed version marker ' + marker);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const { version } = await readPackageInfo();
|
|
322
|
+
await mkdir(dirname(shim), { recursive: true });
|
|
323
|
+
await copyFile(SHIM_SRC, shim);
|
|
324
|
+
await writeFile(
|
|
325
|
+
marker,
|
|
326
|
+
`${JSON.stringify({ version, installedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
327
|
+
'utf8'
|
|
328
|
+
);
|
|
329
|
+
const before = (await readIfExists(config)) ?? '';
|
|
330
|
+
const result = withBlock(before, home);
|
|
331
|
+
if (result.changed) {
|
|
332
|
+
await writeFile(config, result.text, 'utf8');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log('✓ cowork-flow Kimi Code hook shim installed to ' + shim);
|
|
336
|
+
console.log(' Recorded version ' + version + ' in ' + marker);
|
|
337
|
+
console.log(
|
|
338
|
+
' Registered UserPromptSubmit hook in ' + config
|
|
339
|
+
+ (result.changed ? (result.existed ? ' (block updated)' : ' (block appended)') : ' (already up to date)')
|
|
340
|
+
);
|
|
341
|
+
console.log(' Start a new Kimi Code session (or reload the config) for the hook to load.');
|
|
342
|
+
console.log(' Note: injection happens once per submitted prompt; other Kimi Code events cannot inject.');
|
|
343
|
+
}
|
package/src/lib/host-assets.js
CHANGED
|
@@ -19,12 +19,6 @@ const REQUIRED_HOST_NEUTRAL_CAPABILITIES = [
|
|
|
19
19
|
'file_write',
|
|
20
20
|
'party_board_action'
|
|
21
21
|
];
|
|
22
|
-
const REQUIRED_CAPABILITY_MATRIX_HOSTS = [
|
|
23
|
-
'codex',
|
|
24
|
-
'claude-code',
|
|
25
|
-
'opencode',
|
|
26
|
-
'zcode'
|
|
27
|
-
];
|
|
28
22
|
const CAPABILITY_STATUS_VALUES = [
|
|
29
23
|
'native',
|
|
30
24
|
'shim',
|
|
@@ -406,10 +400,7 @@ function validateCapabilityMatrix(matrix, allowed, platformIds) {
|
|
|
406
400
|
if (!matrix.hosts || typeof matrix.hosts !== 'object' || Array.isArray(matrix.hosts)) {
|
|
407
401
|
throw new Error('Host Asset Manifest capabilityMatrix.hosts must be an object');
|
|
408
402
|
}
|
|
409
|
-
const requiredHosts = new Set([
|
|
410
|
-
...REQUIRED_CAPABILITY_MATRIX_HOSTS,
|
|
411
|
-
...platformIds
|
|
412
|
-
]);
|
|
403
|
+
const requiredHosts = new Set([...platformIds]);
|
|
413
404
|
for (const hostId of [...requiredHosts].sort()) {
|
|
414
405
|
if (!matrix.hosts[hostId]) {
|
|
415
406
|
throw new Error(`Host Asset Manifest capability matrix missing host: ${hostId}`);
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "apply_patch|Write|Edit",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": ".cowork-flow/run python .codex/hooks/inject-workflow-state.py",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
13
25
|
]
|
|
14
26
|
}
|
|
15
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
host: kimi-code
|
|
3
|
+
displayName: Kimi Code
|
|
4
|
+
capabilities:
|
|
5
|
+
dispatchSubagent: native
|
|
6
|
+
freshChildContext: native
|
|
7
|
+
sendFollowup: native
|
|
8
|
+
waitChild: native
|
|
9
|
+
listChildren: native
|
|
10
|
+
cancelChild: native
|
|
11
|
+
stateInjection: shim
|
|
12
|
+
backgroundChild: native
|
|
13
|
+
runtimeContextDispatch: native
|
|
14
|
+
runtimeContextBinding: shim
|
|
15
|
+
runtimeContextCleanup: native
|
|
16
|
+
editScopeWarning: unsupported
|
|
17
|
+
dispatch:
|
|
18
|
+
primitive: subagent
|
|
19
|
+
agentsPath: .kimi-code/agents
|
|
20
|
+
skillsPath: .agents/skills
|
|
21
|
+
runtimeContext:
|
|
22
|
+
transport: prompt
|
|
23
|
+
promptKey: cowork_runtime_context_id
|
|
24
|
+
envKey: COWORK_FLOW_RUNTIME_CONTEXT_ID
|
|
25
|
+
metadataKey: cowork_runtime_context_id
|
|
26
|
+
contracts:
|
|
27
|
+
dispatch: RUNTIME_CONTEXT_DISPATCH_V2
|
|
28
|
+
leafExecutor: true
|
|
29
|
+
fallback:
|
|
30
|
+
whenRequiredCapabilityMissing: inline_or_manual
|
|
31
|
+
whenRuntimeContextMissing: fail_closed
|
|
@@ -33,7 +33,7 @@ def main() -> int:
|
|
|
33
33
|
"--phase",
|
|
34
34
|
choices=("edit", "lifecycle"),
|
|
35
35
|
default="lifecycle",
|
|
36
|
-
help="edit: only declarations matching the given files (
|
|
36
|
+
help="edit: only declarations matching the given files (2.5s clamp); "
|
|
37
37
|
"lifecycle: every declaration honoring its files scope.",
|
|
38
38
|
)
|
|
39
39
|
parser.add_argument(
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Assembles one machine-readable fact view per task (task.json, decision-anchor
|
|
6
6
|
essentials, plan binding, bound sessions, trusted snapshot). See
|
|
7
|
-
services/fact_view.py and
|
|
7
|
+
services/fact_view.py and the fact-layer stage 1 design.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -16,6 +16,13 @@ if __package__:
|
|
|
16
16
|
from . import _bootstrap as _bootstrap # noqa: F401
|
|
17
17
|
else:
|
|
18
18
|
import _bootstrap # noqa: F401
|
|
19
|
+
from runtime.host_identity import (
|
|
20
|
+
HostIdentity,
|
|
21
|
+
declared_host,
|
|
22
|
+
detect_host,
|
|
23
|
+
host_ids,
|
|
24
|
+
identity_for,
|
|
25
|
+
)
|
|
19
26
|
from runtime.session_state import (
|
|
20
27
|
resolve_context_key,
|
|
21
28
|
runtime_context_path,
|
|
@@ -66,27 +73,34 @@ def _next_id(base_dir: Path, title: str) -> str:
|
|
|
66
73
|
return candidate
|
|
67
74
|
|
|
68
75
|
|
|
69
|
-
def _detect_host() -> str:
|
|
70
|
-
env
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
def _detect_host() -> str | None:
|
|
77
|
+
"""The host whose per-session env var is present, or None.
|
|
78
|
+
|
|
79
|
+
None is the honest answer when this process carries no host evidence:
|
|
80
|
+
defaulting to another host would file this session's runtime context
|
|
81
|
+
under that host's identity.
|
|
82
|
+
"""
|
|
83
|
+
return detect_host(os.environ)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_host(host_id: str | None) -> HostIdentity:
|
|
87
|
+
"""The host for this dispatch, or ValueError when it cannot be established.
|
|
88
|
+
|
|
89
|
+
Guessing another host would file this session's runtime context under that
|
|
90
|
+
host's identity, so a missing or unregistered host is refused instead.
|
|
91
|
+
"""
|
|
92
|
+
resolved = host_id or declared_host(os.environ) or _detect_host()
|
|
93
|
+
if resolved is None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"host is required when no session host is detectable; "
|
|
96
|
+
f"pass --host with one of: {', '.join(host_ids())}"
|
|
97
|
+
)
|
|
98
|
+
identity = identity_for(resolved)
|
|
99
|
+
if identity is None:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"unknown host: {resolved}; known hosts: {', '.join(host_ids())}"
|
|
102
|
+
)
|
|
103
|
+
return identity
|
|
90
104
|
|
|
91
105
|
|
|
92
106
|
def _host_context_prefix(host: str) -> str:
|
|
@@ -128,13 +142,14 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
128
142
|
base_dir = subagent_contexts_dir(repo_root)
|
|
129
143
|
runtime_context_id = _next_id(base_dir, args.title)
|
|
130
144
|
task_dir = getattr(args, "execution_task_dir", None)
|
|
131
|
-
host = args.host or _detect_host()
|
|
132
|
-
adapter = args.adapter or _detect_adapter(host)
|
|
133
145
|
try:
|
|
146
|
+
identity = _resolve_host(args.host)
|
|
147
|
+
adapter = args.adapter or identity.adapter
|
|
134
148
|
agent_type, dispatch_kind = _resolve_agent_type(args.role, getattr(args, "agent_type", None))
|
|
135
149
|
except ValueError as error:
|
|
136
150
|
print(f"Error: {error}", file=sys.stderr)
|
|
137
151
|
return 1
|
|
152
|
+
host = identity.id
|
|
138
153
|
if dispatch_kind == "formal" and not task_dir:
|
|
139
154
|
print("Error: fixed agent dispatch requires --execution-task-dir", file=sys.stderr)
|
|
140
155
|
return 1
|
|
@@ -19,13 +19,10 @@ from services.context_discovery import (
|
|
|
19
19
|
get_implement_spec,
|
|
20
20
|
)
|
|
21
21
|
from services.task_context import (
|
|
22
|
-
detect_installed_platforms as _detect_installed_platforms,
|
|
23
22
|
discover_spec_files as _discover_spec_files,
|
|
24
23
|
get_check_context,
|
|
25
24
|
get_debug_context,
|
|
26
|
-
is_skill_path as _is_skill_path,
|
|
27
25
|
iter_jsonl_lines as _iter_jsonl_lines,
|
|
28
|
-
skill_path as _skill_path,
|
|
29
26
|
write_jsonl as _write_jsonl,
|
|
30
27
|
)
|
|
31
28
|
from adapters.cli.task_archive_commands import cmd_archive
|
|
@@ -82,7 +82,9 @@ def _resolve_status_task_dir(
|
|
|
82
82
|
print(
|
|
83
83
|
colored(
|
|
84
84
|
"Error: Session binding is process-fallback (shared); "
|
|
85
|
-
"
|
|
85
|
+
"set COWORK_FLOW_CONTEXT_ID to the session value shown "
|
|
86
|
+
"in the workflow-state header, or pass a task dir "
|
|
87
|
+
"explicitly.",
|
|
86
88
|
Colors.RED,
|
|
87
89
|
),
|
|
88
90
|
file=sys.stderr,
|
|
@@ -368,7 +370,8 @@ def cmd_finish(args: argparse.Namespace) -> int:
|
|
|
368
370
|
print(
|
|
369
371
|
colored(
|
|
370
372
|
"Error: Refusing to clear a session binding resolved from "
|
|
371
|
-
"process-fallback identity; archive the task to clear it
|
|
373
|
+
"process-fallback identity; archive the task to clear it, or "
|
|
374
|
+
"set COWORK_FLOW_CONTEXT_ID to your own session identity.",
|
|
372
375
|
Colors.RED,
|
|
373
376
|
),
|
|
374
377
|
file=sys.stderr,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
workflow hook.
|
|
4
4
|
|
|
5
5
|
Rendering stays in workflow_state_hook.py; this module owns only the
|
|
6
|
-
claude-code transport specifics — the
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
claude-code transport specifics — the runtime preamble block and the
|
|
7
|
+
PostToolUse spec-advisory path (stderr + exit 2, shared with codex via the
|
|
8
|
+
neutral helper).
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -24,16 +24,6 @@ CLAUDE_PREAMBLE = (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def claude_session_alias(data: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
-
# Ported from the claude-code wrapper: bare session_id must resolve as a
|
|
29
|
-
# claude session, not fall through to the codex session_id rule.
|
|
30
|
-
if "claude_session_id" not in data:
|
|
31
|
-
session_id = data.get("session_id")
|
|
32
|
-
if isinstance(session_id, str) and session_id.strip():
|
|
33
|
-
data["claude_session_id"] = session_id
|
|
34
|
-
return data
|
|
35
|
-
|
|
36
|
-
|
|
37
27
|
def preamble(root: Path) -> tuple[str, ...]:
|
|
38
28
|
return CLAUDE_PREAMBLE
|
|
39
29
|
|
|
@@ -46,7 +36,6 @@ def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
|
|
|
46
36
|
|
|
47
37
|
POLICY = HostPolicy(
|
|
48
38
|
host="claude-code",
|
|
49
|
-
session_alias=claude_session_alias,
|
|
50
39
|
preamble=preamble,
|
|
51
40
|
post_tool_use=post_tool_use,
|
|
52
41
|
)
|