kimi-plugin-cc 2.0.7
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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +1096 -0
- package/LICENSE +201 -0
- package/README.md +772 -0
- package/agents/kimi-ask.md +82 -0
- package/agents/kimi-challenge.md +82 -0
- package/agents/kimi-pursue.md +91 -0
- package/agents/kimi-rescue.md +85 -0
- package/agents/kimi-review.md +82 -0
- package/agents/kimi-swarm-write.md +92 -0
- package/agents/kimi-swarm.md +83 -0
- package/commands/README.md +16 -0
- package/commands/ask.md +32 -0
- package/commands/cancel.md +17 -0
- package/commands/challenge.md +29 -0
- package/commands/pursue.md +37 -0
- package/commands/replay.md +13 -0
- package/commands/rescue.md +32 -0
- package/commands/result.md +13 -0
- package/commands/review.md +29 -0
- package/commands/setup.md +23 -0
- package/commands/status.md +13 -0
- package/commands/swarm.md +38 -0
- package/dist/background-spawn.js +224 -0
- package/dist/cli-cancellation.js +43 -0
- package/dist/cli-client.js +845 -0
- package/dist/commands/ask.js +327 -0
- package/dist/commands/cancel.js +178 -0
- package/dist/commands/cli-helpers.js +112 -0
- package/dist/commands/models.js +140 -0
- package/dist/commands/not-implemented.js +4 -0
- package/dist/commands/pursue.js +311 -0
- package/dist/commands/registry.js +87 -0
- package/dist/commands/repair-sessions.js +159 -0
- package/dist/commands/replay.js +123 -0
- package/dist/commands/rescue.js +333 -0
- package/dist/commands/result.js +58 -0
- package/dist/commands/review-gate.js +393 -0
- package/dist/commands/review.js +218 -0
- package/dist/commands/setup.js +1132 -0
- package/dist/commands/status.js +27 -0
- package/dist/commands/swarm.js +576 -0
- package/dist/commands/worker.js +31 -0
- package/dist/companion.js +114 -0
- package/dist/config.js +22 -0
- package/dist/errors.js +40 -0
- package/dist/git.js +215 -0
- package/dist/hooks/approval-hook.js +112 -0
- package/dist/hooks/approval-policy.js +254 -0
- package/dist/hooks/config-safety.js +637 -0
- package/dist/hooks/install-paths.js +504 -0
- package/dist/hooks/install.js +281 -0
- package/dist/hooks/managed-block.js +745 -0
- package/dist/hooks/review-gate-stop.js +19 -0
- package/dist/job-store.js +559 -0
- package/dist/jobs.js +202 -0
- package/dist/kimi-command.js +72 -0
- package/dist/kimi-engine.js +624 -0
- package/dist/kimi-errors.js +220 -0
- package/dist/kimi-home.js +9 -0
- package/dist/kimi-timeouts.js +68 -0
- package/dist/kimi-version-probe.js +1410 -0
- package/dist/logging.js +22 -0
- package/dist/native-v2-preflight.js +417 -0
- package/dist/package.json +1 -0
- package/dist/parsing.js +553 -0
- package/dist/paths.js +27 -0
- package/dist/plugin-data.js +98 -0
- package/dist/process-tree.js +448 -0
- package/dist/render.js +159 -0
- package/dist/rescue-approval.js +951 -0
- package/dist/schemas/review-gate-output.js +43 -0
- package/dist/session-title.js +293 -0
- package/dist/stream-json.js +498 -0
- package/dist/types.js +1 -0
- package/dist/vendor/shell-quote/LICENSE +24 -0
- package/dist/vendor/shell-quote/index.js +1 -0
- package/dist/vendor/shell-quote/parse.js +229 -0
- package/dist/vendor/smol-toml/LICENSE +24 -0
- package/dist/vendor/smol-toml/date.js +127 -0
- package/dist/vendor/smol-toml/error.js +63 -0
- package/dist/vendor/smol-toml/extract.js +100 -0
- package/dist/vendor/smol-toml/parse.js +148 -0
- package/dist/vendor/smol-toml/primitive.js +179 -0
- package/dist/vendor/smol-toml/struct.js +184 -0
- package/dist/vendor/smol-toml/util.js +113 -0
- package/dist/version.js +7 -0
- package/docs/migration.md +215 -0
- package/docs/models.md +82 -0
- package/docs/registry-releases.md +70 -0
- package/docs/safety.md +318 -0
- package/docs/session-visibility.md +32 -0
- package/hooks/README.md +9 -0
- package/hooks/hooks.json +16 -0
- package/package.json +67 -0
- package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
- package/plugins/kimi-codex/dist/background-spawn.js +224 -0
- package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-codex/dist/cli-client.js +845 -0
- package/plugins/kimi-codex/dist/commands/ask.js +327 -0
- package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
- package/plugins/kimi-codex/dist/commands/models.js +140 -0
- package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
- package/plugins/kimi-codex/dist/commands/registry.js +87 -0
- package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
- package/plugins/kimi-codex/dist/commands/replay.js +123 -0
- package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
- package/plugins/kimi-codex/dist/commands/result.js +58 -0
- package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
- package/plugins/kimi-codex/dist/commands/review.js +218 -0
- package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
- package/plugins/kimi-codex/dist/commands/status.js +27 -0
- package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
- package/plugins/kimi-codex/dist/commands/worker.js +31 -0
- package/plugins/kimi-codex/dist/companion.js +114 -0
- package/plugins/kimi-codex/dist/config.js +22 -0
- package/plugins/kimi-codex/dist/errors.js +40 -0
- package/plugins/kimi-codex/dist/git.js +215 -0
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
- package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
- package/plugins/kimi-codex/dist/hooks/install.js +281 -0
- package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
- package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-codex/dist/job-store.js +559 -0
- package/plugins/kimi-codex/dist/jobs.js +202 -0
- package/plugins/kimi-codex/dist/kimi-command.js +72 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
- package/plugins/kimi-codex/dist/kimi-home.js +9 -0
- package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
- package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
- package/plugins/kimi-codex/dist/logging.js +22 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-codex/dist/package.json +1 -0
- package/plugins/kimi-codex/dist/parsing.js +553 -0
- package/plugins/kimi-codex/dist/paths.js +27 -0
- package/plugins/kimi-codex/dist/plugin-data.js +98 -0
- package/plugins/kimi-codex/dist/process-tree.js +448 -0
- package/plugins/kimi-codex/dist/render.js +159 -0
- package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
- package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-codex/dist/session-title.js +293 -0
- package/plugins/kimi-codex/dist/stream-json.js +498 -0
- package/plugins/kimi-codex/dist/types.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
- package/plugins/kimi-codex/dist/version.js +7 -0
- package/plugins/kimi-codex/scripts/companion.sh +37 -0
- package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
- package/scripts/companion.sh +37 -0
- package/scripts/review-gate-hook.sh +32 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const MAX_DESCENDANT_DEPTH = 8;
|
|
6
|
+
const MAX_DESCENDANT_PIDS = 512;
|
|
7
|
+
const PROCESS_EXIT_POLL_MS = 10;
|
|
8
|
+
const PROCESS_SNAPSHOT_TIMEOUT_MS = 1_000;
|
|
9
|
+
const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 250;
|
|
10
|
+
const PROBE_TIMEOUT = Symbol("process-probe-timeout");
|
|
11
|
+
/**
|
|
12
|
+
* Capture descendant ancestry and reusable identities in the same bounded
|
|
13
|
+
* process-table snapshot. There is no PID-only handoff: on Linux, PPID, PGID,
|
|
14
|
+
* state, and start time come from one /proc/<pid>/stat read per row; other
|
|
15
|
+
* POSIX systems use one ps invocation for the whole table.
|
|
16
|
+
*/
|
|
17
|
+
export async function collectDescendantIdentities(rootPid) {
|
|
18
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0 || process.platform === "win32")
|
|
19
|
+
return [];
|
|
20
|
+
const snapshot = await raceValueWithTimeout(collectVerifiedDescendantIdentities(rootPid), PROCESS_SNAPSHOT_TIMEOUT_MS);
|
|
21
|
+
if (snapshot === PROBE_TIMEOUT) {
|
|
22
|
+
throw new Error(`process snapshot exceeded ${PROCESS_SNAPSHOT_TIMEOUT_MS}ms`);
|
|
23
|
+
}
|
|
24
|
+
return snapshot;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compatibility surface for callers that only need numeric process listings.
|
|
28
|
+
* Cancellation must use collectDescendantIdentities so ancestry and identity
|
|
29
|
+
* cannot be separated by a PID-reuse window.
|
|
30
|
+
*/
|
|
31
|
+
export async function collectDescendants(rootPid) {
|
|
32
|
+
try {
|
|
33
|
+
return (await collectDescendantIdentities(rootPid)).map((identity) => identity.pid);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Read one reusable process identity. PID alone is not stable across teardown. */
|
|
40
|
+
export async function readProcessIdentity(pid) {
|
|
41
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
42
|
+
return undefined;
|
|
43
|
+
const read = process.platform === "linux"
|
|
44
|
+
? readProcessIdentityBatchFromProc([pid])
|
|
45
|
+
: readProcessIdentityBatchWithPs([pid]);
|
|
46
|
+
const batch = await raceValueWithTimeout(read, PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
|
|
47
|
+
return batch === PROBE_TIMEOUT ? undefined : batch.identities.get(pid);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Revalidate all captured identities concurrently under one deadline. The
|
|
51
|
+
* default path batches ps fallback reads; an injected per-pid reader is fanned
|
|
52
|
+
* out with Promise.all rather than multiplied by descendant count.
|
|
53
|
+
*/
|
|
54
|
+
export async function revalidateProcessIdentities(expectedIdentities, options = {}) {
|
|
55
|
+
const expected = dedupeIdentities(expectedIdentities);
|
|
56
|
+
if (expected.length === 0)
|
|
57
|
+
return { identities: [], complete: true };
|
|
58
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
|
|
59
|
+
const read = options.identityReader === undefined
|
|
60
|
+
? readCurrentIdentityBatch(expected)
|
|
61
|
+
: readIdentityBatchWithReader(expected, options.identityReader);
|
|
62
|
+
const batch = await raceValueWithTimeout(read, timeoutMs);
|
|
63
|
+
if (batch === PROBE_TIMEOUT)
|
|
64
|
+
return { identities: [], complete: false };
|
|
65
|
+
const identities = [];
|
|
66
|
+
for (const expectedIdentity of expected) {
|
|
67
|
+
const current = batch.identities.get(expectedIdentity.pid);
|
|
68
|
+
if (current !== undefined &&
|
|
69
|
+
current.startToken === expectedIdentity.startToken &&
|
|
70
|
+
current.processGroupId === expectedIdentity.processGroupId &&
|
|
71
|
+
!isQuiescentState(current.state)) {
|
|
72
|
+
identities.push(current);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { identities, complete: batch.complete };
|
|
76
|
+
}
|
|
77
|
+
/** Return the current identity only when pid, start token, and PGID still match. */
|
|
78
|
+
export async function revalidateProcessIdentity(expected, identityReader) {
|
|
79
|
+
const validation = await revalidateProcessIdentities([expected], { identityReader });
|
|
80
|
+
return validation.identities[0];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wait until the direct child and every captured identity are quiescent.
|
|
84
|
+
* Every probe is bounded even when the overall wait is infinite, and an
|
|
85
|
+
* incomplete probe is conservatively treated as still active.
|
|
86
|
+
*/
|
|
87
|
+
export async function waitForProcessTreeExit(identities, timeoutMs, options = {}) {
|
|
88
|
+
const targets = dedupeIdentities(identities);
|
|
89
|
+
const finiteTimeout = Number.isFinite(timeoutMs);
|
|
90
|
+
const deadline = finiteTimeout ? Date.now() + Math.max(0, timeoutMs) : Number.POSITIVE_INFINITY;
|
|
91
|
+
while (true) {
|
|
92
|
+
const rootAlive = options.isRootAlive?.() === true;
|
|
93
|
+
if (!rootAlive && targets.length === 0)
|
|
94
|
+
return true;
|
|
95
|
+
const remainingMs = deadline - Date.now();
|
|
96
|
+
if (finiteTimeout && remainingMs <= 0)
|
|
97
|
+
return false;
|
|
98
|
+
let active = rootAlive;
|
|
99
|
+
if (!active) {
|
|
100
|
+
const probeTimeoutMs = finiteTimeout
|
|
101
|
+
? Math.min(PROCESS_IDENTITY_PROBE_TIMEOUT_MS, remainingMs)
|
|
102
|
+
: PROCESS_IDENTITY_PROBE_TIMEOUT_MS;
|
|
103
|
+
const validation = await revalidateProcessIdentities(targets, {
|
|
104
|
+
identityReader: options.identityReader,
|
|
105
|
+
timeoutMs: probeTimeoutMs,
|
|
106
|
+
});
|
|
107
|
+
active = !validation.complete || validation.identities.length > 0;
|
|
108
|
+
}
|
|
109
|
+
if (!active)
|
|
110
|
+
return true;
|
|
111
|
+
const afterProbeRemainingMs = deadline - Date.now();
|
|
112
|
+
if (finiteTimeout && afterProbeRemainingMs <= 0)
|
|
113
|
+
return false;
|
|
114
|
+
await sleep(Math.min(PROCESS_EXIT_POLL_MS, afterProbeRemainingMs));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function collectVerifiedDescendantIdentities(rootPid) {
|
|
118
|
+
const initialRows = await collectProcessSnapshotRows();
|
|
119
|
+
if (initialRows === undefined)
|
|
120
|
+
return [];
|
|
121
|
+
const candidateRows = descendantRowsFromSnapshot(rootPid, initialRows);
|
|
122
|
+
const initialRoot = initialRows.find((row) => row.pid === rootPid);
|
|
123
|
+
if (initialRoot === undefined || candidateRows.length === 0)
|
|
124
|
+
return [];
|
|
125
|
+
// A second, targeted snapshot closes the cross-row /proc race: every child
|
|
126
|
+
// identity and every numeric parent edge must still match before acceptance.
|
|
127
|
+
const expectedRows = [initialRoot, ...candidateRows];
|
|
128
|
+
const expectedByPid = new Map(expectedRows.map((row) => [row.pid, row]));
|
|
129
|
+
const verificationRows = await readCurrentSnapshotRows(expectedRows);
|
|
130
|
+
const stableRows = verificationRows.filter((current) => {
|
|
131
|
+
const expected = expectedByPid.get(current.pid);
|
|
132
|
+
return (expected !== undefined &&
|
|
133
|
+
current.startToken === expected.startToken &&
|
|
134
|
+
current.processGroupId === expected.processGroupId &&
|
|
135
|
+
current.parentPid === expected.parentPid &&
|
|
136
|
+
!isQuiescentState(current.state));
|
|
137
|
+
});
|
|
138
|
+
return descendantRowsFromSnapshot(rootPid, stableRows).map(toProcessIdentity);
|
|
139
|
+
}
|
|
140
|
+
function descendantRowsFromSnapshot(rootPid, rows) {
|
|
141
|
+
const root = rows.find((row) => row.pid === rootPid);
|
|
142
|
+
if (root === undefined || isQuiescentState(root.state))
|
|
143
|
+
return [];
|
|
144
|
+
const childrenByParent = new Map();
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
if (row.pid <= 0 || row.parentPid <= 0 || row.pid === row.parentPid)
|
|
147
|
+
continue;
|
|
148
|
+
const children = childrenByParent.get(row.parentPid);
|
|
149
|
+
if (children === undefined)
|
|
150
|
+
childrenByParent.set(row.parentPid, [row]);
|
|
151
|
+
else
|
|
152
|
+
children.push(row);
|
|
153
|
+
}
|
|
154
|
+
const descendants = [];
|
|
155
|
+
const seen = new Set([rootPid]);
|
|
156
|
+
let frontier = [rootPid];
|
|
157
|
+
for (let depth = 0; depth < MAX_DESCENDANT_DEPTH && frontier.length > 0; depth += 1) {
|
|
158
|
+
const next = [];
|
|
159
|
+
for (const parentPid of frontier) {
|
|
160
|
+
for (const child of childrenByParent.get(parentPid) ?? []) {
|
|
161
|
+
if (seen.has(child.pid))
|
|
162
|
+
continue;
|
|
163
|
+
seen.add(child.pid);
|
|
164
|
+
descendants.push(child);
|
|
165
|
+
if (descendants.length >= MAX_DESCENDANT_PIDS)
|
|
166
|
+
return descendants;
|
|
167
|
+
next.push(child.pid);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
frontier = next;
|
|
171
|
+
}
|
|
172
|
+
return descendants;
|
|
173
|
+
}
|
|
174
|
+
async function readCurrentSnapshotRows(expectedRows) {
|
|
175
|
+
const procPids = [];
|
|
176
|
+
const psPids = [];
|
|
177
|
+
for (const row of expectedRows) {
|
|
178
|
+
if (process.platform === "linux" && row.startToken.startsWith("proc:")) {
|
|
179
|
+
procPids.push(row.pid);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
psPids.push(row.pid);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const [procOutcomes, psRows] = await Promise.all([
|
|
186
|
+
Promise.all(procPids.map((pid) => readProcRow(pid))),
|
|
187
|
+
collectProcessSnapshotRowsFromPs(psPids),
|
|
188
|
+
]);
|
|
189
|
+
return [
|
|
190
|
+
...procOutcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []),
|
|
191
|
+
...(psRows ?? []),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
async function collectProcessSnapshotRows() {
|
|
195
|
+
if (process.platform === "linux") {
|
|
196
|
+
const procRows = await collectProcessSnapshotRowsFromProc();
|
|
197
|
+
if (procRows !== undefined)
|
|
198
|
+
return procRows;
|
|
199
|
+
}
|
|
200
|
+
return collectProcessSnapshotRowsFromPs();
|
|
201
|
+
}
|
|
202
|
+
async function collectProcessSnapshotRowsFromProc() {
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = await readdir("/proc");
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (isErrnoException(err, "ENOENT") || isErrnoException(err, "EPERM"))
|
|
209
|
+
return undefined;
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const outcomes = await Promise.all(entries
|
|
213
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
214
|
+
.map((entry) => readProcRow(Number.parseInt(entry, 10))));
|
|
215
|
+
return outcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []);
|
|
216
|
+
}
|
|
217
|
+
async function collectProcessSnapshotRowsFromPs(pids) {
|
|
218
|
+
const uniquePids = pids === undefined ? undefined : uniqueValidPids(pids);
|
|
219
|
+
if (uniquePids !== undefined && uniquePids.length === 0)
|
|
220
|
+
return [];
|
|
221
|
+
let stdout;
|
|
222
|
+
try {
|
|
223
|
+
const result = await execFileAsync("ps", uniquePids === undefined
|
|
224
|
+
? ["-axo", "pid=,ppid=,pgid=,state=,lstart="]
|
|
225
|
+
: ["-o", "pid=,ppid=,pgid=,state=,lstart=", "-p", uniquePids.join(",")], {
|
|
226
|
+
timeout: uniquePids === undefined
|
|
227
|
+
? PROCESS_SNAPSHOT_TIMEOUT_MS
|
|
228
|
+
: PROCESS_IDENTITY_PROBE_TIMEOUT_MS,
|
|
229
|
+
maxBuffer: uniquePids === undefined ? 2 * 1024 * 1024 : 256 * 1024,
|
|
230
|
+
});
|
|
231
|
+
stdout = result.stdout;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return parsePsSnapshotRows(stdout);
|
|
237
|
+
}
|
|
238
|
+
async function readCurrentIdentityBatch(expected) {
|
|
239
|
+
if (process.platform !== "linux") {
|
|
240
|
+
return readProcessIdentityBatchWithPs(expected.map((identity) => identity.pid));
|
|
241
|
+
}
|
|
242
|
+
const procPids = [];
|
|
243
|
+
const psPids = [];
|
|
244
|
+
for (const identity of expected) {
|
|
245
|
+
if (identity.startToken.startsWith("proc:"))
|
|
246
|
+
procPids.push(identity.pid);
|
|
247
|
+
else
|
|
248
|
+
psPids.push(identity.pid);
|
|
249
|
+
}
|
|
250
|
+
const [procBatch, psBatch] = await Promise.all([
|
|
251
|
+
readProcessIdentityBatchFromProc(procPids),
|
|
252
|
+
readProcessIdentityBatchWithPs(psPids),
|
|
253
|
+
]);
|
|
254
|
+
return mergeIdentityBatches(procBatch, psBatch);
|
|
255
|
+
}
|
|
256
|
+
async function readProcessIdentityBatchFromProc(pids) {
|
|
257
|
+
if (pids.length === 0)
|
|
258
|
+
return { identities: new Map(), complete: true };
|
|
259
|
+
const outcomes = await Promise.all(pids.map((pid) => readProcRow(pid)));
|
|
260
|
+
const identities = new Map();
|
|
261
|
+
let complete = true;
|
|
262
|
+
for (const outcome of outcomes) {
|
|
263
|
+
if (outcome.status === "found") {
|
|
264
|
+
identities.set(outcome.row.pid, toProcessIdentity(outcome.row));
|
|
265
|
+
}
|
|
266
|
+
else if (outcome.status === "error") {
|
|
267
|
+
complete = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { identities, complete };
|
|
271
|
+
}
|
|
272
|
+
async function readProcessIdentityBatchWithPs(pids) {
|
|
273
|
+
const uniquePids = uniqueValidPids(pids);
|
|
274
|
+
if (uniquePids.length === 0)
|
|
275
|
+
return { identities: new Map(), complete: true };
|
|
276
|
+
let stdout;
|
|
277
|
+
try {
|
|
278
|
+
const result = await execFileAsync("ps", ["-o", "pid=,pgid=,state=,lstart=", "-p", uniquePids.join(",")], { timeout: PROCESS_IDENTITY_PROBE_TIMEOUT_MS, maxBuffer: 256 * 1024 });
|
|
279
|
+
stdout = result.stdout;
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
if (isExitCode(err, 1))
|
|
283
|
+
return { identities: new Map(), complete: true };
|
|
284
|
+
return { identities: new Map(), complete: false };
|
|
285
|
+
}
|
|
286
|
+
const identities = new Map();
|
|
287
|
+
for (const line of stdout.split("\n")) {
|
|
288
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
289
|
+
if (match === null)
|
|
290
|
+
continue;
|
|
291
|
+
const pid = Number.parseInt(match[1], 10);
|
|
292
|
+
const processGroupId = Number.parseInt(match[2], 10);
|
|
293
|
+
if (!Number.isInteger(pid) || !Number.isInteger(processGroupId) || processGroupId <= 0) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
identities.set(pid, {
|
|
297
|
+
pid,
|
|
298
|
+
processGroupId,
|
|
299
|
+
state: match[3],
|
|
300
|
+
startToken: psStartToken(match[4]),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return { identities, complete: true };
|
|
304
|
+
}
|
|
305
|
+
async function readIdentityBatchWithReader(expected, identityReader) {
|
|
306
|
+
const outcomes = await Promise.all(expected.map(async (identity) => {
|
|
307
|
+
try {
|
|
308
|
+
return { identity: await identityReader(identity.pid), failed: false };
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return { identity: undefined, failed: true };
|
|
312
|
+
}
|
|
313
|
+
}));
|
|
314
|
+
const identities = new Map();
|
|
315
|
+
let complete = true;
|
|
316
|
+
for (const outcome of outcomes) {
|
|
317
|
+
if (outcome.failed)
|
|
318
|
+
complete = false;
|
|
319
|
+
if (outcome.identity !== undefined)
|
|
320
|
+
identities.set(outcome.identity.pid, outcome.identity);
|
|
321
|
+
}
|
|
322
|
+
return { identities, complete };
|
|
323
|
+
}
|
|
324
|
+
async function readProcRow(pid) {
|
|
325
|
+
let stat;
|
|
326
|
+
try {
|
|
327
|
+
stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
if (isErrnoException(err, "ENOENT") || isErrnoException(err, "ESRCH")) {
|
|
331
|
+
return { status: "missing" };
|
|
332
|
+
}
|
|
333
|
+
return { status: "error" };
|
|
334
|
+
}
|
|
335
|
+
const closeParen = stat.lastIndexOf(")");
|
|
336
|
+
const openParen = stat.indexOf("(");
|
|
337
|
+
if (openParen < 1 || closeParen < openParen)
|
|
338
|
+
return { status: "error" };
|
|
339
|
+
const parsedPid = Number.parseInt(stat.slice(0, openParen).trim(), 10);
|
|
340
|
+
const fields = stat.slice(closeParen + 1).trim().split(/\s+/);
|
|
341
|
+
const state = fields[0];
|
|
342
|
+
const parentPid = Number.parseInt(fields[1] ?? "", 10);
|
|
343
|
+
const processGroupId = Number.parseInt(fields[2] ?? "", 10);
|
|
344
|
+
const startTimeTicks = fields[19];
|
|
345
|
+
if (parsedPid !== pid ||
|
|
346
|
+
state === undefined ||
|
|
347
|
+
startTimeTicks === undefined ||
|
|
348
|
+
!Number.isInteger(parentPid) ||
|
|
349
|
+
!Number.isInteger(processGroupId) ||
|
|
350
|
+
processGroupId <= 0) {
|
|
351
|
+
return { status: "error" };
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
status: "found",
|
|
355
|
+
row: {
|
|
356
|
+
pid,
|
|
357
|
+
parentPid,
|
|
358
|
+
processGroupId,
|
|
359
|
+
startToken: `proc:${startTimeTicks}`,
|
|
360
|
+
state,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function parsePsSnapshotRows(stdout) {
|
|
365
|
+
const rows = [];
|
|
366
|
+
for (const line of stdout.split("\n")) {
|
|
367
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
368
|
+
if (match === null)
|
|
369
|
+
continue;
|
|
370
|
+
const pid = Number.parseInt(match[1], 10);
|
|
371
|
+
const parentPid = Number.parseInt(match[2], 10);
|
|
372
|
+
const processGroupId = Number.parseInt(match[3], 10);
|
|
373
|
+
if (!Number.isInteger(pid) ||
|
|
374
|
+
!Number.isInteger(parentPid) ||
|
|
375
|
+
!Number.isInteger(processGroupId) ||
|
|
376
|
+
processGroupId <= 0) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
rows.push({
|
|
380
|
+
pid,
|
|
381
|
+
parentPid,
|
|
382
|
+
processGroupId,
|
|
383
|
+
state: match[4],
|
|
384
|
+
startToken: psStartToken(match[5]),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return rows;
|
|
388
|
+
}
|
|
389
|
+
function mergeIdentityBatches(first, second) {
|
|
390
|
+
return {
|
|
391
|
+
identities: new Map([...first.identities, ...second.identities]),
|
|
392
|
+
complete: first.complete && second.complete,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function toProcessIdentity(row) {
|
|
396
|
+
return {
|
|
397
|
+
pid: row.pid,
|
|
398
|
+
processGroupId: row.processGroupId,
|
|
399
|
+
startToken: row.startToken,
|
|
400
|
+
state: row.state,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function dedupeIdentities(identities) {
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
const deduped = [];
|
|
406
|
+
for (const identity of identities) {
|
|
407
|
+
const key = `${identity.pid}:${identity.startToken}:${identity.processGroupId}`;
|
|
408
|
+
if (seen.has(key))
|
|
409
|
+
continue;
|
|
410
|
+
seen.add(key);
|
|
411
|
+
deduped.push(identity);
|
|
412
|
+
}
|
|
413
|
+
return deduped;
|
|
414
|
+
}
|
|
415
|
+
function uniqueValidPids(pids) {
|
|
416
|
+
return [...new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
417
|
+
}
|
|
418
|
+
function psStartToken(value) {
|
|
419
|
+
return `ps:${value.replace(/\s+/g, " ").trim()}`;
|
|
420
|
+
}
|
|
421
|
+
function isQuiescentState(state) {
|
|
422
|
+
const normalized = state.trim().toUpperCase();
|
|
423
|
+
return normalized.startsWith("Z") || normalized.startsWith("X");
|
|
424
|
+
}
|
|
425
|
+
function sleep(ms) {
|
|
426
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
427
|
+
}
|
|
428
|
+
async function raceValueWithTimeout(promise, timeoutMs) {
|
|
429
|
+
let timer;
|
|
430
|
+
try {
|
|
431
|
+
return await Promise.race([
|
|
432
|
+
promise,
|
|
433
|
+
new Promise((resolve) => {
|
|
434
|
+
timer = setTimeout(() => resolve(PROBE_TIMEOUT), Math.max(0, timeoutMs));
|
|
435
|
+
}),
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
if (timer !== undefined)
|
|
440
|
+
clearTimeout(timer);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function isErrnoException(err, code) {
|
|
444
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === code;
|
|
445
|
+
}
|
|
446
|
+
function isExitCode(err, code) {
|
|
447
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === code;
|
|
448
|
+
}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getManagedCommandConfig } from "./commands/registry.js";
|
|
4
|
+
import { RuntimeError } from "./errors.js";
|
|
5
|
+
import { parseReviewGateOutput } from "./schemas/review-gate-output.js";
|
|
6
|
+
const EMPTY_SUMMARY_FALLBACK = "Rescue did not return a final message.";
|
|
7
|
+
export async function writeArtifact(paths, job, markdown) {
|
|
8
|
+
const artifactPath = path.join(paths.artifactsDir, `${job.command_type}-${job.job_id}.md`);
|
|
9
|
+
await writeFile(artifactPath, markdown.endsWith("\n") ? markdown : `${markdown}\n`, "utf8");
|
|
10
|
+
return artifactPath;
|
|
11
|
+
}
|
|
12
|
+
export async function readArtifact(artifactPath) {
|
|
13
|
+
return readFile(artifactPath, "utf8");
|
|
14
|
+
}
|
|
15
|
+
export function renderManagedJobOutput(job, finalText) {
|
|
16
|
+
assertOutputModeConsistency(job.command_type, finalText);
|
|
17
|
+
switch (job.command_type) {
|
|
18
|
+
case "ask": {
|
|
19
|
+
const trimmed = finalText.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
throw new RuntimeError("ASK_EMPTY_OUTPUT", "ask returned an empty final response.", "ask.prompt");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
output: trimmed,
|
|
25
|
+
rendered: renderAskArtifact(trimmed),
|
|
26
|
+
summary: trimmed.slice(0, 160),
|
|
27
|
+
error: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
case "review":
|
|
31
|
+
case "challenge": {
|
|
32
|
+
const trimmed = finalText.trim();
|
|
33
|
+
if (!trimmed) {
|
|
34
|
+
throw new RuntimeError(job.command_type === "challenge" ? "CHALLENGE_EMPTY_OUTPUT" : "REVIEW_EMPTY_OUTPUT", `${job.command_type} returned an empty final response.`, `${job.command_type}.prompt`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
output: trimmed,
|
|
38
|
+
rendered: renderReviewArtifact(job, trimmed),
|
|
39
|
+
summary: firstMeaningfulLine(trimmed),
|
|
40
|
+
error: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "review_gate": {
|
|
44
|
+
const output = parseReviewGateOutput(finalText.trim());
|
|
45
|
+
return {
|
|
46
|
+
output,
|
|
47
|
+
rendered: renderReviewGateArtifact(job, output),
|
|
48
|
+
summary: output.summary,
|
|
49
|
+
error: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
case "rescue": {
|
|
53
|
+
const trimmed = finalText.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
throw new RuntimeError("RESCUE_EMPTY_OUTPUT", "Rescue returned empty output.", "rescue.runtime");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
output: finalText,
|
|
59
|
+
rendered: renderRescueArtifact(finalText),
|
|
60
|
+
summary: firstMeaningfulLine(finalText),
|
|
61
|
+
error: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
return assertNever(job.command_type);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function assertOutputModeConsistency(commandType, finalText) {
|
|
69
|
+
const { outputMode } = getManagedCommandConfig(commandType);
|
|
70
|
+
switch (outputMode) {
|
|
71
|
+
case "passthrough":
|
|
72
|
+
return;
|
|
73
|
+
case "parsed":
|
|
74
|
+
try {
|
|
75
|
+
JSON.parse(finalText);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new RuntimeError(`${commandType.toUpperCase()}_PARSE_FAILED`, `${commandType} is configured for parsed output but returned malformed JSON: ${error.message}`, `${commandType}.parse`, error instanceof Error ? { cause: error } : undefined);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
default:
|
|
82
|
+
return assertNever(outputMode);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function renderAskArtifact(output) {
|
|
86
|
+
return output.trim();
|
|
87
|
+
}
|
|
88
|
+
export function renderReviewArtifact(job, output) {
|
|
89
|
+
const header = [
|
|
90
|
+
`# ${job.command_type === "challenge" ? "Challenge" : "Review"} Result`,
|
|
91
|
+
"",
|
|
92
|
+
`- Job: ${job.job_id}`,
|
|
93
|
+
...(job.kimi_session_id ? [`- Kimi session: ${job.kimi_session_id}`] : []),
|
|
94
|
+
].join("\n");
|
|
95
|
+
// Blank line between header and body, plus trailing newline so the rendered
|
|
96
|
+
// string is well-formed even before writeArtifact's newline normalization.
|
|
97
|
+
return `${header}\n\n${output.trim()}\n`;
|
|
98
|
+
}
|
|
99
|
+
export function renderRescueArtifact(rawOutput) {
|
|
100
|
+
return rawOutput.endsWith("\n") ? rawOutput : `${rawOutput}\n`;
|
|
101
|
+
}
|
|
102
|
+
export function firstMeaningfulLine(text, fallback = EMPTY_SUMMARY_FALLBACK) {
|
|
103
|
+
const line = String(text ?? "")
|
|
104
|
+
.split(/\r?\n/)
|
|
105
|
+
.map((value) => value.trim())
|
|
106
|
+
.find(Boolean);
|
|
107
|
+
return line ?? fallback;
|
|
108
|
+
}
|
|
109
|
+
export function renderReviewGateArtifact(job, output) {
|
|
110
|
+
const lines = [
|
|
111
|
+
"# Review Gate Result",
|
|
112
|
+
"",
|
|
113
|
+
`- Job: ${job.job_id}`,
|
|
114
|
+
`- Decision: ${output.decision}`,
|
|
115
|
+
`- Confidence: ${output.confidence}`,
|
|
116
|
+
`- Summary: ${output.summary}`,
|
|
117
|
+
...(job.kimi_session_id ? [`- Kimi session: ${job.kimi_session_id}`] : []),
|
|
118
|
+
];
|
|
119
|
+
if (output.issues.length === 0) {
|
|
120
|
+
lines.push("", "No issues.");
|
|
121
|
+
return `${lines.join("\n")}\n`;
|
|
122
|
+
}
|
|
123
|
+
lines.push("", "## Issues");
|
|
124
|
+
for (const issue of output.issues) {
|
|
125
|
+
lines.push("", `### ${issue.title}`, `- Severity: ${issue.severity}`, issue.body);
|
|
126
|
+
}
|
|
127
|
+
return `${lines.join("\n")}\n`;
|
|
128
|
+
}
|
|
129
|
+
export function renderTerminalJobArtifact(job) {
|
|
130
|
+
const lines = [
|
|
131
|
+
`# ${capitalize(job.status)} Job`,
|
|
132
|
+
"",
|
|
133
|
+
`- Job: ${job.job_id}`,
|
|
134
|
+
`- Command: ${job.command_type}`,
|
|
135
|
+
`- Status: ${job.status}`,
|
|
136
|
+
`- Summary: ${job.summary}`,
|
|
137
|
+
...(job.kimi_session_id ? [`- Kimi session: ${job.kimi_session_id}`] : []),
|
|
138
|
+
];
|
|
139
|
+
if (job.error) {
|
|
140
|
+
lines.push("", "## Error", `- Code: ${job.error.code}`, `- Stage: ${job.error.stage}`, job.error.message);
|
|
141
|
+
// Structured context (e.g. `drift_axis` / `retryable_after_setup`) so a
|
|
142
|
+
// caller reading the rendered artifact — not just `result --json` — can
|
|
143
|
+
// branch on it. Deterministic key order keeps artifacts stable.
|
|
144
|
+
const details = job.error.details;
|
|
145
|
+
if (details !== undefined && Object.keys(details).length > 0) {
|
|
146
|
+
lines.push("", "### Details");
|
|
147
|
+
for (const key of Object.keys(details).sort()) {
|
|
148
|
+
lines.push(`- ${key}: ${JSON.stringify(details[key])}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
function capitalize(value) {
|
|
155
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
156
|
+
}
|
|
157
|
+
function assertNever(value) {
|
|
158
|
+
throw new RuntimeError("UNSUPPORTED_COMMAND_TYPE", `Unsupported command type for rendering: ${String(value)}`, "render");
|
|
159
|
+
}
|