specrails-core 5.6.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -145
- package/bin/specrails-core.mjs +12 -554
- package/dist/agent-runtime/capabilities.js +0 -1
- package/dist/agent-runtime/change-scope.d.ts +63 -0
- package/dist/agent-runtime/change-scope.js +297 -0
- package/dist/agent-runtime/cli-executor.d.ts +1 -1
- package/dist/agent-runtime/cli-executor.js +1 -2
- package/dist/agent-runtime/cli-process.js +0 -1
- package/dist/agent-runtime/cli.js +4 -5
- package/dist/agent-runtime/codex-schema.js +0 -1
- package/dist/agent-runtime/compact/architect.d.ts +3 -272
- package/dist/agent-runtime/compact/architect.js +8 -9
- package/dist/agent-runtime/compact/chat-client.d.ts +3 -3
- package/dist/agent-runtime/compact/chat-client.js +1 -2
- package/dist/agent-runtime/compact/developer.d.ts +3 -70
- package/dist/agent-runtime/compact/developer.js +13 -14
- package/dist/agent-runtime/compact/environment.d.ts +16 -5
- package/dist/agent-runtime/compact/environment.js +32 -3
- package/dist/agent-runtime/compact/exit-code-honesty.d.ts +2 -1
- package/dist/agent-runtime/compact/exit-code-honesty.js +0 -1
- package/dist/agent-runtime/compact/guarded-loop.d.ts +2 -18
- package/dist/agent-runtime/compact/guarded-loop.js +7 -8
- package/dist/agent-runtime/compact/prompt-inputs.d.ts +0 -2
- package/dist/agent-runtime/compact/prompt-inputs.js +1 -2
- package/dist/agent-runtime/compact/reviewer.d.ts +0 -13
- package/dist/agent-runtime/compact/reviewer.js +5 -6
- package/dist/agent-runtime/compact/step.d.ts +0 -1
- package/dist/agent-runtime/compact/step.js +1 -2
- package/dist/agent-runtime/compact/test-reachability.js +6 -3
- package/dist/agent-runtime/config.js +0 -1
- package/dist/agent-runtime/core-host.d.ts +2 -2
- package/dist/agent-runtime/core-host.js +2 -3
- package/dist/agent-runtime/durable-store.d.ts +0 -1
- package/dist/agent-runtime/durable-store.js +1 -2
- package/dist/agent-runtime/efficiency-summary.d.ts +2 -2
- package/dist/agent-runtime/efficiency-summary.js +1 -2
- package/dist/agent-runtime/efficiency-types.js +0 -1
- package/dist/agent-runtime/efficiency.js +0 -1
- package/dist/agent-runtime/evaluation-corpus.js +0 -1
- package/dist/agent-runtime/evaluation.d.ts +2 -1
- package/dist/agent-runtime/evaluation.js +1 -2
- package/dist/agent-runtime/executor-types.d.ts +1 -1
- package/dist/agent-runtime/executor-types.js +0 -1
- package/dist/agent-runtime/executors.js +0 -1
- package/dist/agent-runtime/gemini-policy.js +0 -1
- package/dist/agent-runtime/graph/artifacts.d.ts +1 -1
- package/dist/agent-runtime/graph/artifacts.js +5 -3
- package/dist/agent-runtime/graph/convergence.d.ts +26 -0
- package/dist/agent-runtime/graph/convergence.js +62 -0
- package/dist/agent-runtime/graph/nodes.d.ts +1 -1
- package/dist/agent-runtime/graph/nodes.js +170 -24
- package/dist/agent-runtime/graph/review-policy.js +0 -1
- package/dist/agent-runtime/graph/roles.d.ts +2 -5
- package/dist/agent-runtime/graph/roles.js +2 -8
- package/dist/agent-runtime/graph/state.d.ts +6 -1
- package/dist/agent-runtime/graph/state.js +2 -1
- package/dist/agent-runtime/graph-checkpointer.d.ts +2 -1
- package/dist/agent-runtime/graph-checkpointer.js +0 -1
- package/dist/agent-runtime/guardrails.d.ts +3 -3
- package/dist/agent-runtime/guardrails.js +1 -2
- package/dist/agent-runtime/index.d.ts +1 -1
- package/dist/agent-runtime/index.js +1 -2
- package/dist/agent-runtime/kimi-acp.js +1 -2
- package/dist/agent-runtime/kimi-model.d.ts +6 -0
- package/dist/agent-runtime/kimi-model.js +12 -0
- package/dist/agent-runtime/openai-executor.d.ts +0 -9
- package/dist/agent-runtime/openai-executor.js +6 -7
- package/dist/agent-runtime/openspec-tool-server.js +1 -2
- package/dist/agent-runtime/openspec.d.ts +0 -2
- package/dist/agent-runtime/openspec.js +1 -6
- package/dist/agent-runtime/prompts.d.ts +11 -5
- package/dist/agent-runtime/prompts.js +46 -17
- package/dist/agent-runtime/provider-diagnostic.js +1 -2
- package/dist/agent-runtime/recovery.js +1 -2
- package/dist/agent-runtime/repository-context.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +19 -1
- package/dist/agent-runtime/review-context.d.ts +1 -1
- package/dist/agent-runtime/review-context.js +0 -1
- package/dist/agent-runtime/role-routing.js +0 -1
- package/dist/agent-runtime/role-state.js +0 -1
- package/dist/agent-runtime/runtime-identity.js +1 -2
- package/dist/agent-runtime/tool-event.js +0 -1
- package/dist/agent-runtime/verification-plan.d.ts +5 -4
- package/dist/agent-runtime/verification-plan.js +9 -5
- package/dist/agent-runtime/workflow-types.js +0 -1
- package/dist/agent-runtime/workflow.js +0 -1
- package/dist/agent-runtime/workspace-tools.d.ts +2 -2
- package/dist/agent-runtime/workspace-tools.js +1 -2
- package/dist/installer/cli.d.ts +3 -32
- package/dist/installer/cli.js +26 -101
- package/dist/installer/commands/framework.d.ts +4 -3
- package/dist/installer/commands/framework.js +0 -1
- package/dist/installer/commands/init.d.ts +4 -25
- package/dist/installer/commands/init.js +5 -8
- package/dist/installer/phases/framework-lifecycle.d.ts +3 -17
- package/dist/installer/phases/framework-lifecycle.js +2 -3
- package/dist/installer/phases/install-config.d.ts +7 -8
- package/dist/installer/phases/install-config.js +2 -7
- package/dist/installer/phases/manifest.d.ts +6 -7
- package/dist/installer/phases/manifest.js +2 -7
- package/dist/installer/phases/prereqs.d.ts +6 -7
- package/dist/installer/phases/prereqs.js +4 -6
- package/dist/installer/phases/provider-detect.d.ts +6 -7
- package/dist/installer/phases/provider-detect.js +2 -4
- package/dist/installer/phases/scaffold.d.ts +9 -10
- package/dist/installer/phases/scaffold.js +82 -805
- package/dist/installer/util/errors.js +0 -1
- package/dist/installer/util/exec.d.ts +3 -2
- package/dist/installer/util/exec.js +0 -1
- package/dist/installer/util/fs.d.ts +0 -10
- package/dist/installer/util/fs.js +1 -2
- package/dist/installer/util/git.js +0 -1
- package/dist/installer/util/install-transaction.d.ts +2 -2
- package/dist/installer/util/install-transaction.js +1 -2
- package/dist/installer/util/logger.d.ts +1 -3
- package/dist/installer/util/logger.js +0 -1
- package/dist/installer/util/paths.js +0 -1
- package/dist/installer/util/registry.d.ts +4 -8
- package/dist/installer/util/registry.js +2 -3
- package/dist/{installer/runtime → pipeline}/pipeline-state.d.ts +11 -0
- package/dist/{installer/runtime → pipeline}/pipeline-state.js +56 -5
- package/dist/shared/args.d.ts +18 -0
- package/dist/shared/args.js +53 -0
- package/integration-contract.json +11 -47
- package/package.json +3 -10
- package/templates/agents/sr-architect.md +15 -310
- package/templates/agents/sr-developer.md +15 -269
- package/templates/agents/sr-reviewer.md +13 -367
- package/templates/codex-skills/rails/sr-architect/SKILL.md +14 -308
- package/templates/codex-skills/rails/sr-developer/SKILL.md +14 -201
- package/templates/codex-skills/rails/sr-reviewer/SKILL.md +15 -289
- package/templates/kimi/specrails/run-skill.mjs +91 -2173
- package/bin/tui-installer.mjs +0 -430
- package/commands/doctor.md +0 -62
- package/dist/agent-runtime/capabilities.js.map +0 -1
- package/dist/agent-runtime/cli-executor.js.map +0 -1
- package/dist/agent-runtime/cli-process.js.map +0 -1
- package/dist/agent-runtime/cli.js.map +0 -1
- package/dist/agent-runtime/codex-schema.js.map +0 -1
- package/dist/agent-runtime/compact/architect.js.map +0 -1
- package/dist/agent-runtime/compact/chat-client.js.map +0 -1
- package/dist/agent-runtime/compact/developer.js.map +0 -1
- package/dist/agent-runtime/compact/environment.js.map +0 -1
- package/dist/agent-runtime/compact/exit-code-honesty.js.map +0 -1
- package/dist/agent-runtime/compact/guarded-loop.js.map +0 -1
- package/dist/agent-runtime/compact/prompt-inputs.js.map +0 -1
- package/dist/agent-runtime/compact/reviewer.js.map +0 -1
- package/dist/agent-runtime/compact/step.js.map +0 -1
- package/dist/agent-runtime/compact/test-reachability.js.map +0 -1
- package/dist/agent-runtime/config.js.map +0 -1
- package/dist/agent-runtime/core-host.js.map +0 -1
- package/dist/agent-runtime/durable-store.js.map +0 -1
- package/dist/agent-runtime/efficiency-summary.js.map +0 -1
- package/dist/agent-runtime/efficiency-types.js.map +0 -1
- package/dist/agent-runtime/efficiency.js.map +0 -1
- package/dist/agent-runtime/evaluation-corpus.js.map +0 -1
- package/dist/agent-runtime/evaluation.js.map +0 -1
- package/dist/agent-runtime/executor-types.js.map +0 -1
- package/dist/agent-runtime/executors.js.map +0 -1
- package/dist/agent-runtime/gemini-policy.js.map +0 -1
- package/dist/agent-runtime/graph/artifacts.js.map +0 -1
- package/dist/agent-runtime/graph/nodes.js.map +0 -1
- package/dist/agent-runtime/graph/review-policy.js.map +0 -1
- package/dist/agent-runtime/graph/roles.js.map +0 -1
- package/dist/agent-runtime/graph/state.js.map +0 -1
- package/dist/agent-runtime/graph-checkpointer.js.map +0 -1
- package/dist/agent-runtime/guardrails.js.map +0 -1
- package/dist/agent-runtime/index.js.map +0 -1
- package/dist/agent-runtime/kimi-acp.js.map +0 -1
- package/dist/agent-runtime/openai-executor.js.map +0 -1
- package/dist/agent-runtime/openspec-tool-server.js.map +0 -1
- package/dist/agent-runtime/openspec.js.map +0 -1
- package/dist/agent-runtime/prompts.js.map +0 -1
- package/dist/agent-runtime/provider-diagnostic.js.map +0 -1
- package/dist/agent-runtime/recovery.js.map +0 -1
- package/dist/agent-runtime/repository-context.js.map +0 -1
- package/dist/agent-runtime/review-context.js.map +0 -1
- package/dist/agent-runtime/role-routing.js.map +0 -1
- package/dist/agent-runtime/role-state.js.map +0 -1
- package/dist/agent-runtime/runtime-identity.js.map +0 -1
- package/dist/agent-runtime/tool-event.js.map +0 -1
- package/dist/agent-runtime/verification-plan.js.map +0 -1
- package/dist/agent-runtime/workflow-types.js.map +0 -1
- package/dist/agent-runtime/workflow.js.map +0 -1
- package/dist/agent-runtime/workspace-tools.js.map +0 -1
- package/dist/installer/cli.js.map +0 -1
- package/dist/installer/commands/doctor.d.ts +0 -27
- package/dist/installer/commands/doctor.js +0 -615
- package/dist/installer/commands/doctor.js.map +0 -1
- package/dist/installer/commands/framework.js.map +0 -1
- package/dist/installer/commands/init.js.map +0 -1
- package/dist/installer/commands/update.d.ts +0 -56
- package/dist/installer/commands/update.js +0 -257
- package/dist/installer/commands/update.js.map +0 -1
- package/dist/installer/commands/v5-migration.d.ts +0 -32
- package/dist/installer/commands/v5-migration.js +0 -133
- package/dist/installer/commands/v5-migration.js.map +0 -1
- package/dist/installer/phases/framework-lifecycle.js.map +0 -1
- package/dist/installer/phases/install-config.js.map +0 -1
- package/dist/installer/phases/manifest.js.map +0 -1
- package/dist/installer/phases/prereqs.js.map +0 -1
- package/dist/installer/phases/provider-detect.js.map +0 -1
- package/dist/installer/phases/scaffold.js.map +0 -1
- package/dist/installer/runtime/kimi.d.ts +0 -84
- package/dist/installer/runtime/kimi.js +0 -255
- package/dist/installer/runtime/kimi.js.map +0 -1
- package/dist/installer/runtime/pipeline-state.js.map +0 -1
- package/dist/installer/util/errors.js.map +0 -1
- package/dist/installer/util/exec.js.map +0 -1
- package/dist/installer/util/fs.js.map +0 -1
- package/dist/installer/util/git.js.map +0 -1
- package/dist/installer/util/install-transaction.js.map +0 -1
- package/dist/installer/util/logger.js.map +0 -1
- package/dist/installer/util/paths.js.map +0 -1
- package/dist/installer/util/prompts.d.ts +0 -23
- package/dist/installer/util/prompts.js +0 -49
- package/dist/installer/util/prompts.js.map +0 -1
- package/dist/installer/util/registry.js.map +0 -1
- package/dist/installer/util/template.d.ts +0 -23
- package/dist/installer/util/template.js +0 -60
- package/dist/installer/util/template.js.map +0 -1
- package/docs/README.md +0 -40
- package/docs/agent-runtime-efficiency.md +0 -65
- package/docs/agent-runtime.md +0 -393
- package/docs/agents.md +0 -315
- package/docs/changelog.md +0 -168
- package/docs/ci-cd.md +0 -60
- package/docs/concepts.md +0 -183
- package/docs/customization.md +0 -363
- package/docs/deployment.md +0 -234
- package/docs/getting-started.md +0 -122
- package/docs/installation.md +0 -395
- package/docs/local-tickets.md +0 -191
- package/docs/migration-guide.md +0 -140
- package/docs/playbook-oss-maintainer.md +0 -112
- package/docs/playbook-parallel-dev.md +0 -124
- package/docs/playbook-product-discovery.md +0 -115
- package/docs/plugin-architecture.md +0 -144
- package/docs/research/codex-compatibility-analysis.md +0 -365
- package/docs/research/mcp-feasibility-analysis.md +0 -382
- package/docs/testing/test-matrix-codex.md +0 -130
- package/docs/updating.md +0 -89
- package/docs/user-docs/cli-reference.md +0 -428
- package/docs/user-docs/codex-vs-claude-code.md +0 -23
- package/docs/user-docs/core-updates.md +0 -70
- package/docs/user-docs/faq.md +0 -146
- package/docs/user-docs/getting-started-codex.md +0 -194
- package/docs/user-docs/getting-started-kimi.md +0 -423
- package/docs/user-docs/installation.md +0 -237
- package/docs/user-docs/provider-pipelines.md +0 -96
- package/docs/user-docs/quick-start.md +0 -161
- package/docs/windows.md +0 -78
- package/docs/workflows.md +0 -463
- package/schemas/profile.v1.json +0 -207
- package/templates/claude-md/CLAUDE-quickstart.md +0 -54
- package/templates/claude-md/root.md +0 -50
- package/templates/codex-skills/batch-implement/SKILL.md +0 -38
- package/templates/codex-skills/implement/SKILL.md +0 -38
- package/templates/codex-skills/retry/SKILL.md +0 -16
- package/templates/commands/specrails/compat-check.md +0 -271
- package/templates/commands/specrails/doctor.md +0 -62
- package/templates/commands/specrails/explore-spec.md +0 -173
- package/templates/commands/specrails/health-check.md +0 -527
- package/templates/commands/specrails/memory-inspect.md +0 -261
- package/templates/commands/specrails/opsx-diff.md +0 -419
- package/templates/commands/specrails/propose-spec.md +0 -102
- package/templates/commands/specrails/refactor-recommender.md +0 -169
- package/templates/commands/specrails/telemetry.md +0 -553
- package/templates/commands/specrails/why.md +0 -96
- package/templates/gemini-commands/batch-implement.toml +0 -37
- package/templates/gemini-commands/implement.toml +0 -37
- package/templates/gemini-commands/retry.toml +0 -15
- package/templates/profiles/default.json +0 -14
- package/templates/profiles/kimi-default.json +0 -15
- package/templates/rules/layer.md +0 -8
- package/templates/runtime/provider-pipeline.md +0 -113
- package/templates/security/security-exemptions.yaml +0 -20
- package/templates/settings/confidence-config.json +0 -17
- package/templates/settings/settings.json +0 -15
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
5
4
|
import {
|
|
6
5
|
accessSync,
|
|
7
|
-
copyFileSync,
|
|
8
|
-
cpSync,
|
|
9
|
-
chmodSync,
|
|
10
6
|
constants as fsConstants,
|
|
11
7
|
existsSync,
|
|
12
8
|
lstatSync,
|
|
13
|
-
mkdirSync,
|
|
14
9
|
readFileSync,
|
|
15
|
-
readdirSync,
|
|
16
|
-
readlinkSync,
|
|
17
10
|
realpathSync,
|
|
18
|
-
renameSync,
|
|
19
|
-
rmSync,
|
|
20
11
|
statSync,
|
|
21
|
-
symlinkSync,
|
|
22
|
-
unlinkSync,
|
|
23
|
-
writeFileSync,
|
|
24
12
|
} from 'node:fs'
|
|
25
|
-
import os from 'node:os'
|
|
26
13
|
import path from 'node:path'
|
|
27
|
-
import { StringDecoder } from 'node:string_decoder'
|
|
28
14
|
import { fileURLToPath } from 'node:url'
|
|
29
15
|
|
|
30
16
|
import { load as loadYaml } from './vendor/js-yaml/js-yaml.mjs'
|
|
@@ -32,28 +18,9 @@ import { load as loadYaml } from './vendor/js-yaml/js-yaml.mjs'
|
|
|
32
18
|
const SAFE_SKILL_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
33
19
|
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._/:-]*$/
|
|
34
20
|
const SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/
|
|
35
|
-
const SAFE_WAVE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/
|
|
36
|
-
const SAFE_GIT_OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/
|
|
37
21
|
const MAX_MODEL_ID_LENGTH = 128
|
|
38
22
|
const MAX_SESSION_ID_LENGTH = 128
|
|
39
|
-
const
|
|
40
|
-
const MAX_ROLE_WAVE_SIZE = 32
|
|
41
|
-
const ROLE_REQUEST_RELATIVE_PATH = path.join(
|
|
42
|
-
'.specrails',
|
|
43
|
-
'kimi-role-request.json',
|
|
44
|
-
)
|
|
45
|
-
const ROLE_WAVE_RELATIVE_PATH = path.join(
|
|
46
|
-
'.specrails',
|
|
47
|
-
'kimi-role-wave.json',
|
|
48
|
-
)
|
|
49
|
-
const ROLE_MERGE_RELATIVE_PATH = path.join(
|
|
50
|
-
'.specrails',
|
|
51
|
-
'kimi-role-merge.json',
|
|
52
|
-
)
|
|
53
|
-
const ROLE_WORKTREE_MANIFEST_DIR = path.join(
|
|
54
|
-
'.specrails',
|
|
55
|
-
'kimi-role-worktrees',
|
|
56
|
-
)
|
|
23
|
+
const MAX_EXECUTION_CONTEXT_BYTES = 1_048_576
|
|
57
24
|
const WINDOWS_COMMAND_LINE_BUDGET = 30_000
|
|
58
25
|
const MAX_MANAGED_PROMPT_BYTES = 1_048_576
|
|
59
26
|
const PLAIN_PROMPT_STDIN_FLAG = '--plain-prompt-stdin'
|
|
@@ -87,11 +54,6 @@ const VALUE_FLAGS = new Set([
|
|
|
87
54
|
'--add-dir',
|
|
88
55
|
'--attachment',
|
|
89
56
|
'--prompt',
|
|
90
|
-
'--request-file',
|
|
91
|
-
'--role-wave-file',
|
|
92
|
-
'--role-wave-status',
|
|
93
|
-
'--role-wave-cleanup',
|
|
94
|
-
'--role-merge-file',
|
|
95
57
|
])
|
|
96
58
|
|
|
97
59
|
export class RunnerUsageError extends Error {
|
|
@@ -268,11 +230,6 @@ export function parseRunnerArgs(argv) {
|
|
|
268
230
|
additionalDirs: [],
|
|
269
231
|
attachmentPaths: [],
|
|
270
232
|
extraPrompt: undefined,
|
|
271
|
-
requestFile: undefined,
|
|
272
|
-
roleWaveFile: undefined,
|
|
273
|
-
roleWaveStatus: undefined,
|
|
274
|
-
roleWaveCleanup: undefined,
|
|
275
|
-
roleMergeFile: undefined,
|
|
276
233
|
plainPromptStdin: false,
|
|
277
234
|
}
|
|
278
235
|
let positionalArgs
|
|
@@ -333,16 +290,6 @@ export function parseRunnerArgs(argv) {
|
|
|
333
290
|
parsed.sessionId = value
|
|
334
291
|
} else if (token === '--prompt') {
|
|
335
292
|
parsed.extraPrompt = value
|
|
336
|
-
} else if (token === '--request-file') {
|
|
337
|
-
parsed.requestFile = value
|
|
338
|
-
} else if (token === '--role-wave-file') {
|
|
339
|
-
parsed.roleWaveFile = value
|
|
340
|
-
} else if (token === '--role-wave-status') {
|
|
341
|
-
parsed.roleWaveStatus = value
|
|
342
|
-
} else if (token === '--role-wave-cleanup') {
|
|
343
|
-
parsed.roleWaveCleanup = value
|
|
344
|
-
} else if (token === '--role-merge-file') {
|
|
345
|
-
parsed.roleMergeFile = value
|
|
346
293
|
}
|
|
347
294
|
}
|
|
348
295
|
}
|
|
@@ -354,85 +301,7 @@ export function parseRunnerArgs(argv) {
|
|
|
354
301
|
parsed.rawArgs = positionalArgs
|
|
355
302
|
}
|
|
356
303
|
|
|
357
|
-
if (
|
|
358
|
-
parsed.requestFile !== undefined ||
|
|
359
|
-
parsed.roleWaveFile !== undefined ||
|
|
360
|
-
parsed.roleWaveStatus !== undefined ||
|
|
361
|
-
parsed.roleWaveCleanup !== undefined ||
|
|
362
|
-
parsed.roleMergeFile !== undefined
|
|
363
|
-
) {
|
|
364
|
-
const modes = [
|
|
365
|
-
parsed.requestFile !== undefined,
|
|
366
|
-
parsed.roleWaveFile !== undefined,
|
|
367
|
-
parsed.roleWaveStatus !== undefined,
|
|
368
|
-
parsed.roleWaveCleanup !== undefined,
|
|
369
|
-
parsed.roleMergeFile !== undefined,
|
|
370
|
-
].filter(Boolean).length
|
|
371
|
-
if (modes !== 1) {
|
|
372
|
-
throw new RunnerUsageError(
|
|
373
|
-
'Use only one request, role-wave, status, or merge mode',
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
const requestFlag =
|
|
377
|
-
parsed.roleWaveFile !== undefined
|
|
378
|
-
? '--role-wave-file'
|
|
379
|
-
: parsed.roleWaveStatus !== undefined
|
|
380
|
-
? '--role-wave-status'
|
|
381
|
-
: parsed.roleWaveCleanup !== undefined
|
|
382
|
-
? '--role-wave-cleanup'
|
|
383
|
-
: parsed.roleMergeFile !== undefined
|
|
384
|
-
? '--role-merge-file'
|
|
385
|
-
: '--request-file'
|
|
386
|
-
for (const flag of [
|
|
387
|
-
'--skill',
|
|
388
|
-
'--model',
|
|
389
|
-
'--args',
|
|
390
|
-
'--session',
|
|
391
|
-
'--prompt',
|
|
392
|
-
]) {
|
|
393
|
-
if (seenSingleValueFlags.has(flag)) {
|
|
394
|
-
throw new RunnerUsageError(
|
|
395
|
-
`${requestFlag} cannot be combined with ${flag}`,
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (positionalArgs !== undefined) {
|
|
400
|
-
throw new RunnerUsageError(
|
|
401
|
-
`${requestFlag} cannot be combined with positional arguments`,
|
|
402
|
-
)
|
|
403
|
-
}
|
|
404
|
-
if (parsed.plainPromptStdin) {
|
|
405
|
-
throw new RunnerUsageError(
|
|
406
|
-
`${requestFlag} cannot be combined with ${PLAIN_PROMPT_STDIN_FLAG}`,
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
if (parsed.requestFile !== undefined) {
|
|
410
|
-
parsed.requestFile = requireNonEmpty(
|
|
411
|
-
parsed.requestFile,
|
|
412
|
-
'request file',
|
|
413
|
-
)
|
|
414
|
-
} else if (parsed.roleWaveFile !== undefined) {
|
|
415
|
-
parsed.roleWaveFile = requireNonEmpty(
|
|
416
|
-
parsed.roleWaveFile,
|
|
417
|
-
'role wave file',
|
|
418
|
-
)
|
|
419
|
-
} else if (parsed.roleWaveStatus !== undefined) {
|
|
420
|
-
parsed.roleWaveStatus = requireSafeWaveId(
|
|
421
|
-
parsed.roleWaveStatus,
|
|
422
|
-
'role wave status run',
|
|
423
|
-
)
|
|
424
|
-
} else if (parsed.roleWaveCleanup !== undefined) {
|
|
425
|
-
parsed.roleWaveCleanup = requireSafeWaveId(
|
|
426
|
-
parsed.roleWaveCleanup,
|
|
427
|
-
'role wave cleanup run',
|
|
428
|
-
)
|
|
429
|
-
} else {
|
|
430
|
-
parsed.roleMergeFile = requireNonEmpty(
|
|
431
|
-
parsed.roleMergeFile,
|
|
432
|
-
'role merge file',
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
|
-
} else if (parsed.plainPromptStdin) {
|
|
304
|
+
if (parsed.plainPromptStdin) {
|
|
436
305
|
for (const flag of ['--skill', '--args', '--prompt', '--attachment']) {
|
|
437
306
|
if (
|
|
438
307
|
seenSingleValueFlags.has(flag) ||
|
|
@@ -463,342 +332,6 @@ export function parseRunnerArgs(argv) {
|
|
|
463
332
|
return parsed
|
|
464
333
|
}
|
|
465
334
|
|
|
466
|
-
export function loadRoleRequest(parsed, cwd) {
|
|
467
|
-
if (parsed.requestFile === undefined) return parsed
|
|
468
|
-
const requestPath = path.resolve(cwd, parsed.requestFile)
|
|
469
|
-
const expectedPath = path.resolve(cwd, ROLE_REQUEST_RELATIVE_PATH)
|
|
470
|
-
if (requestPath !== expectedPath) {
|
|
471
|
-
throw new RunnerUsageError(
|
|
472
|
-
`Role request must use ${ROLE_REQUEST_RELATIVE_PATH}`,
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
assertManagedPathParents(cwd, requestPath, 'role request')
|
|
476
|
-
|
|
477
|
-
let metadata
|
|
478
|
-
try {
|
|
479
|
-
metadata = lstatSync(requestPath)
|
|
480
|
-
} catch (error) {
|
|
481
|
-
throw new RunnerUsageError(
|
|
482
|
-
`Cannot inspect role request ${requestPath}: ${errorMessage(error)}`,
|
|
483
|
-
)
|
|
484
|
-
}
|
|
485
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
486
|
-
throw new RunnerUsageError(
|
|
487
|
-
`Role request must be a regular non-symlink file: ${requestPath}`,
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
if (metadata.size > MAX_ROLE_REQUEST_BYTES) {
|
|
491
|
-
unlinkSync(requestPath)
|
|
492
|
-
throw new RunnerUsageError(
|
|
493
|
-
`Role request exceeds ${MAX_ROLE_REQUEST_BYTES} bytes`,
|
|
494
|
-
)
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
let source
|
|
498
|
-
try {
|
|
499
|
-
source = readFileSync(requestPath, 'utf8')
|
|
500
|
-
} finally {
|
|
501
|
-
// The fixed request is one-shot. Removing it before spawn prevents stale
|
|
502
|
-
// context from being reused by a later role invocation.
|
|
503
|
-
unlinkSync(requestPath)
|
|
504
|
-
}
|
|
505
|
-
if (Buffer.byteLength(source, 'utf8') > MAX_ROLE_REQUEST_BYTES) {
|
|
506
|
-
throw new RunnerUsageError(
|
|
507
|
-
`Role request exceeds ${MAX_ROLE_REQUEST_BYTES} bytes`,
|
|
508
|
-
)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
let request
|
|
512
|
-
try {
|
|
513
|
-
request = JSON.parse(source)
|
|
514
|
-
} catch (error) {
|
|
515
|
-
throw new RunnerUsageError(
|
|
516
|
-
`Invalid role request JSON: ${errorMessage(error)}`,
|
|
517
|
-
)
|
|
518
|
-
}
|
|
519
|
-
if (!isRecord(request)) {
|
|
520
|
-
throw new RunnerUsageError('Role request must be a JSON object')
|
|
521
|
-
}
|
|
522
|
-
const keys = Object.keys(request).sort()
|
|
523
|
-
if (
|
|
524
|
-
keys.length !== 3 ||
|
|
525
|
-
keys[0] !== 'args' ||
|
|
526
|
-
keys[1] !== 'model' ||
|
|
527
|
-
keys[2] !== 'skill'
|
|
528
|
-
) {
|
|
529
|
-
throw new RunnerUsageError(
|
|
530
|
-
'Role request must contain exactly skill, model, and args',
|
|
531
|
-
)
|
|
532
|
-
}
|
|
533
|
-
if (typeof request.args !== 'string') {
|
|
534
|
-
throw new RunnerUsageError('Role request args must be a string')
|
|
535
|
-
}
|
|
536
|
-
assertNoNul(request.args, 'role request args')
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
...parsed,
|
|
540
|
-
skill: requireSafeSkillId(request.skill),
|
|
541
|
-
model: requireSafeModelId(request.model),
|
|
542
|
-
rawArgs: request.args.trim(),
|
|
543
|
-
requestFile: undefined,
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export function loadRoleWave(parsed, cwd) {
|
|
548
|
-
if (parsed.roleWaveFile === undefined) return undefined
|
|
549
|
-
const requestPath = path.resolve(cwd, parsed.roleWaveFile)
|
|
550
|
-
const expectedPath = path.resolve(cwd, ROLE_WAVE_RELATIVE_PATH)
|
|
551
|
-
if (requestPath !== expectedPath) {
|
|
552
|
-
throw new RunnerUsageError(
|
|
553
|
-
`Role wave must use ${ROLE_WAVE_RELATIVE_PATH}`,
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
assertManagedPathParents(cwd, requestPath, 'role wave')
|
|
557
|
-
|
|
558
|
-
let metadata
|
|
559
|
-
try {
|
|
560
|
-
metadata = lstatSync(requestPath)
|
|
561
|
-
} catch (error) {
|
|
562
|
-
throw new RunnerUsageError(
|
|
563
|
-
`Cannot inspect role wave ${requestPath}: ${errorMessage(error)}`,
|
|
564
|
-
)
|
|
565
|
-
}
|
|
566
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
567
|
-
throw new RunnerUsageError(
|
|
568
|
-
`Role wave must be a regular non-symlink file: ${requestPath}`,
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
if (metadata.size > MAX_ROLE_REQUEST_BYTES) {
|
|
572
|
-
unlinkSync(requestPath)
|
|
573
|
-
throw new RunnerUsageError(
|
|
574
|
-
`Role wave exceeds ${MAX_ROLE_REQUEST_BYTES} bytes`,
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
let source
|
|
579
|
-
try {
|
|
580
|
-
source = readFileSync(requestPath, 'utf8')
|
|
581
|
-
} finally {
|
|
582
|
-
// A whole wave is one-shot. Deleting before any worktree or process is
|
|
583
|
-
// created prevents stale context and removes the parallel WriteFile race.
|
|
584
|
-
unlinkSync(requestPath)
|
|
585
|
-
}
|
|
586
|
-
if (Buffer.byteLength(source, 'utf8') > MAX_ROLE_REQUEST_BYTES) {
|
|
587
|
-
throw new RunnerUsageError(
|
|
588
|
-
`Role wave exceeds ${MAX_ROLE_REQUEST_BYTES} bytes`,
|
|
589
|
-
)
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
let request
|
|
593
|
-
try {
|
|
594
|
-
request = JSON.parse(source)
|
|
595
|
-
} catch (error) {
|
|
596
|
-
throw new RunnerUsageError(
|
|
597
|
-
`Invalid role wave JSON: ${errorMessage(error)}`,
|
|
598
|
-
)
|
|
599
|
-
}
|
|
600
|
-
if (!isRecord(request)) {
|
|
601
|
-
throw new RunnerUsageError('Role wave must be a JSON object')
|
|
602
|
-
}
|
|
603
|
-
const requestKeys = Object.keys(request).sort()
|
|
604
|
-
if (
|
|
605
|
-
requestKeys.length !== 2 ||
|
|
606
|
-
requestKeys[0] !== 'roles' ||
|
|
607
|
-
requestKeys[1] !== 'run'
|
|
608
|
-
) {
|
|
609
|
-
throw new RunnerUsageError(
|
|
610
|
-
'Role wave must contain exactly run and roles',
|
|
611
|
-
)
|
|
612
|
-
}
|
|
613
|
-
const run = requireSafeWaveId(request.run, 'run')
|
|
614
|
-
if (
|
|
615
|
-
!Array.isArray(request.roles) ||
|
|
616
|
-
request.roles.length === 0 ||
|
|
617
|
-
request.roles.length > MAX_ROLE_WAVE_SIZE
|
|
618
|
-
) {
|
|
619
|
-
throw new RunnerUsageError(
|
|
620
|
-
`Role wave roles must contain 1-${MAX_ROLE_WAVE_SIZE} entries`,
|
|
621
|
-
)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const seenKeys = new Set()
|
|
625
|
-
const seenWorktrees = new Set()
|
|
626
|
-
const roles = request.roles.map((value, index) => {
|
|
627
|
-
if (!isRecord(value)) {
|
|
628
|
-
throw new RunnerUsageError(
|
|
629
|
-
`Role wave entry ${index} must be a JSON object`,
|
|
630
|
-
)
|
|
631
|
-
}
|
|
632
|
-
const keys = Object.keys(value).sort()
|
|
633
|
-
if (
|
|
634
|
-
keys.length !== 6 ||
|
|
635
|
-
keys[0] !== 'args' ||
|
|
636
|
-
keys[1] !== 'key' ||
|
|
637
|
-
keys[2] !== 'model' ||
|
|
638
|
-
keys[3] !== 'profile' ||
|
|
639
|
-
keys[4] !== 'skill' ||
|
|
640
|
-
keys[5] !== 'workspace'
|
|
641
|
-
) {
|
|
642
|
-
throw new RunnerUsageError(
|
|
643
|
-
`Role wave entry ${index} must contain exactly ` +
|
|
644
|
-
'key, skill, model, profile, args, and workspace',
|
|
645
|
-
)
|
|
646
|
-
}
|
|
647
|
-
const key = requireSafeWaveId(value.key, `role key ${index}`)
|
|
648
|
-
if (seenKeys.has(key)) {
|
|
649
|
-
throw new RunnerUsageError(`Duplicate role wave key: ${key}`)
|
|
650
|
-
}
|
|
651
|
-
seenKeys.add(key)
|
|
652
|
-
if (typeof value.args !== 'string') {
|
|
653
|
-
throw new RunnerUsageError(`Role wave entry ${key} args must be a string`)
|
|
654
|
-
}
|
|
655
|
-
assertNoNul(value.args, `role wave entry ${key} args`)
|
|
656
|
-
if (typeof value.workspace !== 'string') {
|
|
657
|
-
throw new RunnerUsageError(
|
|
658
|
-
`Role wave entry ${key} workspace must be a string`,
|
|
659
|
-
)
|
|
660
|
-
}
|
|
661
|
-
let workspace = value.workspace
|
|
662
|
-
if (workspace !== 'current') {
|
|
663
|
-
if (!workspace.startsWith('worktree:')) {
|
|
664
|
-
throw new RunnerUsageError(
|
|
665
|
-
`Role wave entry ${key} workspace must be current or worktree:<id>`,
|
|
666
|
-
)
|
|
667
|
-
}
|
|
668
|
-
const worktreeId = requireSafeWaveId(
|
|
669
|
-
workspace.slice('worktree:'.length),
|
|
670
|
-
`worktree id for ${key}`,
|
|
671
|
-
)
|
|
672
|
-
workspace = `worktree:${worktreeId}`
|
|
673
|
-
if (seenWorktrees.has(workspace)) {
|
|
674
|
-
throw new RunnerUsageError(
|
|
675
|
-
`Concurrent roles cannot share workspace ${workspace}`,
|
|
676
|
-
)
|
|
677
|
-
}
|
|
678
|
-
seenWorktrees.add(workspace)
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
key,
|
|
682
|
-
skill: requireSafeSkillId(value.skill),
|
|
683
|
-
model: requireSafeModelId(value.model),
|
|
684
|
-
profile:
|
|
685
|
-
value.profile === 'inherit'
|
|
686
|
-
? 'inherit'
|
|
687
|
-
: requireSafeWaveId(value.profile, `profile for ${key}`),
|
|
688
|
-
rawArgs: value.args.trim(),
|
|
689
|
-
workspace,
|
|
690
|
-
}
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
return {
|
|
694
|
-
run,
|
|
695
|
-
roles,
|
|
696
|
-
additionalDirs: parsed.additionalDirs,
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
export function loadRoleMerge(parsed, cwd) {
|
|
701
|
-
if (parsed.roleMergeFile === undefined) return undefined
|
|
702
|
-
const requestPath = path.resolve(cwd, parsed.roleMergeFile)
|
|
703
|
-
const expectedPath = path.resolve(cwd, ROLE_MERGE_RELATIVE_PATH)
|
|
704
|
-
if (requestPath !== expectedPath) {
|
|
705
|
-
throw new RunnerUsageError(
|
|
706
|
-
`Role merge must use ${ROLE_MERGE_RELATIVE_PATH}`,
|
|
707
|
-
)
|
|
708
|
-
}
|
|
709
|
-
assertManagedPathParents(cwd, requestPath, 'role merge')
|
|
710
|
-
let metadata
|
|
711
|
-
try {
|
|
712
|
-
metadata = lstatSync(requestPath)
|
|
713
|
-
} catch (error) {
|
|
714
|
-
throw new RunnerUsageError(
|
|
715
|
-
`Cannot inspect role merge ${requestPath}: ${errorMessage(error)}`,
|
|
716
|
-
)
|
|
717
|
-
}
|
|
718
|
-
if (
|
|
719
|
-
!metadata.isFile() ||
|
|
720
|
-
metadata.isSymbolicLink() ||
|
|
721
|
-
metadata.size > MAX_ROLE_REQUEST_BYTES
|
|
722
|
-
) {
|
|
723
|
-
if (metadata.isFile() && !metadata.isSymbolicLink()) {
|
|
724
|
-
unlinkSync(requestPath)
|
|
725
|
-
}
|
|
726
|
-
throw new RunnerUsageError(
|
|
727
|
-
`Role merge must be a regular file at most ${MAX_ROLE_REQUEST_BYTES} bytes`,
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
let source
|
|
731
|
-
try {
|
|
732
|
-
source = readFileSync(requestPath, 'utf8')
|
|
733
|
-
} finally {
|
|
734
|
-
unlinkSync(requestPath)
|
|
735
|
-
}
|
|
736
|
-
let request
|
|
737
|
-
try {
|
|
738
|
-
request = JSON.parse(source)
|
|
739
|
-
} catch (error) {
|
|
740
|
-
throw new RunnerUsageError(
|
|
741
|
-
`Invalid role merge JSON: ${errorMessage(error)}`,
|
|
742
|
-
)
|
|
743
|
-
}
|
|
744
|
-
if (
|
|
745
|
-
!isRecord(request) ||
|
|
746
|
-
Object.keys(request).sort().join(',') !== 'actions,run'
|
|
747
|
-
) {
|
|
748
|
-
throw new RunnerUsageError(
|
|
749
|
-
'Role merge must contain exactly run and actions',
|
|
750
|
-
)
|
|
751
|
-
}
|
|
752
|
-
const run = requireSafeWaveId(request.run, 'role merge run')
|
|
753
|
-
if (
|
|
754
|
-
!Array.isArray(request.actions) ||
|
|
755
|
-
request.actions.length === 0 ||
|
|
756
|
-
request.actions.length > 4_096
|
|
757
|
-
) {
|
|
758
|
-
throw new RunnerUsageError(
|
|
759
|
-
'Role merge actions must contain 1-4096 entries',
|
|
760
|
-
)
|
|
761
|
-
}
|
|
762
|
-
const targets = new Set()
|
|
763
|
-
const actions = request.actions.map((value, index) => {
|
|
764
|
-
if (
|
|
765
|
-
!isRecord(value) ||
|
|
766
|
-
Object.keys(value).sort().join(',') !==
|
|
767
|
-
'operation,path,worktree'
|
|
768
|
-
) {
|
|
769
|
-
throw new RunnerUsageError(
|
|
770
|
-
`Role merge action ${index} must contain exactly ` +
|
|
771
|
-
'worktree, path, and operation',
|
|
772
|
-
)
|
|
773
|
-
}
|
|
774
|
-
const worktree = requireSafeWaveId(
|
|
775
|
-
value.worktree,
|
|
776
|
-
`role merge worktree ${index}`,
|
|
777
|
-
)
|
|
778
|
-
const relativePath = requireSafeRelativePath(
|
|
779
|
-
value.path,
|
|
780
|
-
`role merge path ${index}`,
|
|
781
|
-
)
|
|
782
|
-
if (value.operation !== 'copy' && value.operation !== 'delete') {
|
|
783
|
-
throw new RunnerUsageError(
|
|
784
|
-
`Role merge action ${index} operation must be copy or delete`,
|
|
785
|
-
)
|
|
786
|
-
}
|
|
787
|
-
if (targets.has(relativePath)) {
|
|
788
|
-
throw new RunnerUsageError(
|
|
789
|
-
`Role merge contains duplicate target path: ${relativePath}`,
|
|
790
|
-
)
|
|
791
|
-
}
|
|
792
|
-
targets.add(relativePath)
|
|
793
|
-
return {
|
|
794
|
-
worktree,
|
|
795
|
-
path: relativePath,
|
|
796
|
-
operation: value.operation,
|
|
797
|
-
}
|
|
798
|
-
})
|
|
799
|
-
return { run, actions }
|
|
800
|
-
}
|
|
801
|
-
|
|
802
335
|
export function prepareSkillLaunch(options, dependencies = {}) {
|
|
803
336
|
const readFile = dependencies.readFile ?? ((file) => readFileSync(file, 'utf8'))
|
|
804
337
|
const resolvePath = dependencies.resolvePath ?? realpathSync
|
|
@@ -899,10 +432,7 @@ function canonicalizeExistingPath(value) {
|
|
|
899
432
|
|
|
900
433
|
/**
|
|
901
434
|
* Node's legacy realpath implementation can preserve an input's Windows 8.3
|
|
902
|
-
* spelling
|
|
903
|
-
* versa). Use the native resolver and a case-folded identity for comparisons;
|
|
904
|
-
* callers still lstat the recomputed managed path so a symlink alias cannot
|
|
905
|
-
* satisfy an ownership check.
|
|
435
|
+
* spelling. Use the native resolver so --add-dir receives the long name.
|
|
906
436
|
*/
|
|
907
437
|
function canonicalRealpath(value) {
|
|
908
438
|
const resolved =
|
|
@@ -912,13 +442,6 @@ function canonicalRealpath(value) {
|
|
|
912
442
|
return path.resolve(resolved)
|
|
913
443
|
}
|
|
914
444
|
|
|
915
|
-
function existingPathIdentity(value) {
|
|
916
|
-
const canonical = canonicalRealpath(value)
|
|
917
|
-
return process.platform === 'win32'
|
|
918
|
-
? canonical.toLowerCase()
|
|
919
|
-
: canonical
|
|
920
|
-
}
|
|
921
|
-
|
|
922
445
|
function validateAttachmentPaths(attachmentPaths = [], dependencies = {}) {
|
|
923
446
|
const inspect = dependencies.lstat ?? lstatSync
|
|
924
447
|
const resolvePath = dependencies.resolvePath ?? realpathSync
|
|
@@ -930,1379 +453,114 @@ function validateAttachmentPaths(attachmentPaths = [], dependencies = {}) {
|
|
|
930
453
|
return attachmentPaths.map((attachment) => {
|
|
931
454
|
if (!path.isAbsolute(attachment)) {
|
|
932
455
|
throw new RunnerUsageError(
|
|
933
|
-
`Attachment path must be absolute: ${attachment}`,
|
|
934
|
-
)
|
|
935
|
-
}
|
|
936
|
-
try {
|
|
937
|
-
const metadata = inspect(attachment)
|
|
938
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
939
|
-
throw new Error('not a regular non-symlink file')
|
|
940
|
-
}
|
|
941
|
-
const canonical = resolvePath(attachment)
|
|
942
|
-
assertReadable(canonical)
|
|
943
|
-
return canonical
|
|
944
|
-
} catch (error) {
|
|
945
|
-
throw new RunnerUsageError(
|
|
946
|
-
`Attachment must be a readable regular non-symlink file: ` +
|
|
947
|
-
`${attachment}: ${errorMessage(error)}`,
|
|
948
|
-
)
|
|
949
|
-
}
|
|
950
|
-
})
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
export function parseNpmCmdShimEntry(shimPath, contents) {
|
|
954
|
-
const match = contents.match(
|
|
955
|
-
/%dp0%[\\/]([^"\r\n]*?\.(?:mjs|cjs|js))["']?\s+%\*/i,
|
|
956
|
-
)
|
|
957
|
-
if (!match?.[1]) return null
|
|
958
|
-
return path.win32.join(path.win32.dirname(shimPath), match[1])
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
export function resolveWindowsKimiBinary(env = process.env, fileExists = existsSync) {
|
|
962
|
-
const pathValue = getEnvCaseInsensitive(env, 'PATH') ?? ''
|
|
963
|
-
// npm installs three siblings on Windows: `kimi`, `kimi.cmd`, and
|
|
964
|
-
// `kimi.ps1`. The extensionless sibling is a POSIX shell script and cannot
|
|
965
|
-
// be passed to CreateProcess, while PowerShell scripts cannot be spawned
|
|
966
|
-
// directly with shell:false. Probe only executable-safe forms, with the npm
|
|
967
|
-
// command shim first so large prompts can use the stdin bootstrap.
|
|
968
|
-
const names = ['kimi.cmd', 'kimi.bat', 'kimi.exe', 'kimi.com']
|
|
969
|
-
|
|
970
|
-
for (const rawEntry of pathValue.split(';')) {
|
|
971
|
-
const entry = rawEntry.trim().replace(/^"(.*)"$/, '$1')
|
|
972
|
-
if (entry === '') continue
|
|
973
|
-
for (const name of names) {
|
|
974
|
-
const candidate = path.win32.join(entry, name)
|
|
975
|
-
if (fileExists(candidate)) return candidate
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
throw new RunnerUsageError(
|
|
979
|
-
'No shell-free Kimi executable (.cmd, .bat, .exe, or .com) was found on PATH',
|
|
980
|
-
)
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
export function resolveKimiLaunch(kimiArgs, options = {}) {
|
|
984
|
-
const platform = options.platform ?? process.platform
|
|
985
|
-
if (platform !== 'win32') {
|
|
986
|
-
return { command: 'kimi', args: kimiArgs }
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const fileExists = options.fileExists ?? existsSync
|
|
990
|
-
const readFile = options.readFile ?? ((file) => readFileSync(file, 'utf8'))
|
|
991
|
-
const binary = options.binary ?? resolveWindowsKimiBinary(options.env, fileExists)
|
|
992
|
-
const extension = path.win32.extname(binary).toLowerCase()
|
|
993
|
-
if (extension !== '.cmd' && extension !== '.bat') {
|
|
994
|
-
assertWindowsCommandLineBudget(binary, kimiArgs)
|
|
995
|
-
return { command: binary, args: kimiArgs }
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
let entry
|
|
999
|
-
try {
|
|
1000
|
-
entry = parseNpmCmdShimEntry(binary, readFile(binary))
|
|
1001
|
-
} catch (error) {
|
|
1002
|
-
throw new RunnerUsageError(
|
|
1003
|
-
`Cannot read the Kimi Windows shim ${binary}: ${errorMessage(error)}`,
|
|
1004
|
-
)
|
|
1005
|
-
}
|
|
1006
|
-
if (entry === null) {
|
|
1007
|
-
throw new RunnerUsageError(
|
|
1008
|
-
`Refusing to execute non-standard Kimi Windows shim through cmd.exe: ${binary}`,
|
|
1009
|
-
)
|
|
1010
|
-
}
|
|
1011
|
-
const localNode = path.win32.join(path.win32.dirname(binary), 'node.exe')
|
|
1012
|
-
const nodeBinary = fileExists(localNode) ? localNode : 'node'
|
|
1013
|
-
const promptFlag = kimiArgs.indexOf('-p')
|
|
1014
|
-
if (promptFlag === -1 || promptFlag + 1 >= kimiArgs.length) {
|
|
1015
|
-
const args = [entry, ...kimiArgs]
|
|
1016
|
-
assertWindowsCommandLineBudget(nodeBinary, args)
|
|
1017
|
-
return { command: nodeBinary, args }
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const transportedArgs = [...kimiArgs]
|
|
1021
|
-
const prompt = transportedArgs[promptFlag + 1]
|
|
1022
|
-
transportedArgs[promptFlag + 1] = WINDOWS_PROMPT_STDIN_TOKEN
|
|
1023
|
-
const args = [
|
|
1024
|
-
'-e',
|
|
1025
|
-
WINDOWS_NPM_STDIN_BOOTSTRAP,
|
|
1026
|
-
entry,
|
|
1027
|
-
...transportedArgs,
|
|
1028
|
-
]
|
|
1029
|
-
assertWindowsCommandLineBudget(nodeBinary, args)
|
|
1030
|
-
return { command: nodeBinary, args, stdinText: prompt }
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
export function materializeRoleWaveWorkspaces(wave, options = {}) {
|
|
1034
|
-
const initialCwd = canonicalRealpath(options.cwd ?? process.cwd())
|
|
1035
|
-
const git = options.spawnSync ?? spawnSync
|
|
1036
|
-
const gitBinary = options.gitBinary ?? 'git'
|
|
1037
|
-
const tempRoot = ensureManagedTempRoot(options.tempRoot ?? os.tmpdir())
|
|
1038
|
-
const providerRoot = canonicalRealpath(options.providerRoot)
|
|
1039
|
-
const runGit = (cwd, args, input, extraEnv = {}) => {
|
|
1040
|
-
const result = git(
|
|
1041
|
-
gitBinary,
|
|
1042
|
-
['-C', cwd, '-c', 'core.autocrlf=false', ...args],
|
|
1043
|
-
{
|
|
1044
|
-
encoding: 'utf8',
|
|
1045
|
-
env: { ...process.env, ...extraEnv },
|
|
1046
|
-
input,
|
|
1047
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
1048
|
-
shell: false,
|
|
1049
|
-
},
|
|
1050
|
-
)
|
|
1051
|
-
if (result.error || result.status !== 0) {
|
|
1052
|
-
const detail =
|
|
1053
|
-
errorMessage(result.error ?? '').trim() ||
|
|
1054
|
-
String(result.stderr ?? '').trim() ||
|
|
1055
|
-
`exit ${String(result.status)}`
|
|
1056
|
-
throw new RunnerUsageError(
|
|
1057
|
-
`git ${args.join(' ')} failed in ${cwd}: ${detail}`,
|
|
1058
|
-
)
|
|
1059
|
-
}
|
|
1060
|
-
return String(result.stdout ?? '')
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
const baseRepo = canonicalRealpath(
|
|
1064
|
-
runGit(initialCwd, ['rev-parse', '--show-toplevel']).trim(),
|
|
1065
|
-
)
|
|
1066
|
-
const manifestPath = path.join(
|
|
1067
|
-
baseRepo,
|
|
1068
|
-
ROLE_WORKTREE_MANIFEST_DIR,
|
|
1069
|
-
`${wave.run}.json`,
|
|
1070
|
-
)
|
|
1071
|
-
ensureRealDirectoryTree(baseRepo, path.dirname(manifestPath))
|
|
1072
|
-
const existingManifest = readRoleWaveManifest(manifestPath)
|
|
1073
|
-
const head = requireGitOid(
|
|
1074
|
-
runGit(baseRepo, ['rev-parse', 'HEAD']).trim(),
|
|
1075
|
-
'repository HEAD',
|
|
1076
|
-
)
|
|
1077
|
-
if (
|
|
1078
|
-
existingManifest &&
|
|
1079
|
-
(
|
|
1080
|
-
existingManifest.run !== wave.run ||
|
|
1081
|
-
existingPathIdentity(existingManifest.baseRepo) !==
|
|
1082
|
-
existingPathIdentity(baseRepo)
|
|
1083
|
-
)
|
|
1084
|
-
) {
|
|
1085
|
-
throw new RunnerUsageError(
|
|
1086
|
-
`Role wave manifest does not belong to ${wave.run} in ${baseRepo}`,
|
|
1087
|
-
)
|
|
1088
|
-
}
|
|
1089
|
-
const worktrees = { ...(existingManifest?.worktrees ?? {}) }
|
|
1090
|
-
const repoKey = createHash('sha256')
|
|
1091
|
-
.update(baseRepo)
|
|
1092
|
-
.digest('hex')
|
|
1093
|
-
.slice(0, 16)
|
|
1094
|
-
const needsWorktrees = wave.roles.some(
|
|
1095
|
-
(role) => role.workspace !== 'current',
|
|
1096
|
-
)
|
|
1097
|
-
let baseCommit = existingManifest?.baseCommit ?? null
|
|
1098
|
-
let sourceHead = existingManifest?.sourceHead ?? null
|
|
1099
|
-
let createdBaseline = false
|
|
1100
|
-
if (baseCommit !== null) {
|
|
1101
|
-
baseCommit = requireGitOid(baseCommit, 'role wave base commit')
|
|
1102
|
-
runGit(baseRepo, ['cat-file', '-e', `${baseCommit}^{commit}`])
|
|
1103
|
-
} else if (needsWorktrees) {
|
|
1104
|
-
sourceHead = head
|
|
1105
|
-
baseCommit = createSyntheticBaselineCommit({
|
|
1106
|
-
baseRepo,
|
|
1107
|
-
head,
|
|
1108
|
-
repoKey,
|
|
1109
|
-
run: wave.run,
|
|
1110
|
-
tempRoot,
|
|
1111
|
-
runGit,
|
|
1112
|
-
})
|
|
1113
|
-
createdBaseline = true
|
|
1114
|
-
}
|
|
1115
|
-
const worktreeRoot = path.join(
|
|
1116
|
-
tempRoot,
|
|
1117
|
-
'specrails-kimi-worktrees',
|
|
1118
|
-
repoKey,
|
|
1119
|
-
wave.run,
|
|
1120
|
-
)
|
|
1121
|
-
const executionRoot = path.join(
|
|
1122
|
-
tempRoot,
|
|
1123
|
-
'specrails-kimi-execution',
|
|
1124
|
-
repoKey,
|
|
1125
|
-
wave.run,
|
|
1126
|
-
)
|
|
1127
|
-
ensureRealDirectoryTree(tempRoot, worktreeRoot)
|
|
1128
|
-
ensureRealDirectoryTree(tempRoot, executionRoot)
|
|
1129
|
-
const gitExcludeDirectory = path.join(
|
|
1130
|
-
tempRoot,
|
|
1131
|
-
'specrails-kimi-git-excludes',
|
|
1132
|
-
repoKey,
|
|
1133
|
-
)
|
|
1134
|
-
ensureRealDirectoryTree(tempRoot, gitExcludeDirectory)
|
|
1135
|
-
const gitExcludeFile = writeRoleGitExclude(
|
|
1136
|
-
path.join(gitExcludeDirectory, `${wave.run}.txt`),
|
|
1137
|
-
)
|
|
1138
|
-
|
|
1139
|
-
const listed = parseGitWorktreeList(
|
|
1140
|
-
runGit(baseRepo, ['worktree', 'list', '--porcelain', '-z']),
|
|
1141
|
-
)
|
|
1142
|
-
if (existingManifest) {
|
|
1143
|
-
validateRoleWaveManifestIntegrity(existingManifest, {
|
|
1144
|
-
baseRepo,
|
|
1145
|
-
run: wave.run,
|
|
1146
|
-
tempRoot,
|
|
1147
|
-
repoKey,
|
|
1148
|
-
registered: listed,
|
|
1149
|
-
runGit: (args) => runGit(baseRepo, args),
|
|
1150
|
-
})
|
|
1151
|
-
}
|
|
1152
|
-
const createdThisCall = []
|
|
1153
|
-
try {
|
|
1154
|
-
const roles = wave.roles.map((role) => {
|
|
1155
|
-
if (role.profile !== 'inherit') {
|
|
1156
|
-
const profilePath = path.join(
|
|
1157
|
-
baseRepo,
|
|
1158
|
-
'.specrails',
|
|
1159
|
-
'profiles',
|
|
1160
|
-
`${role.profile}.json`,
|
|
1161
|
-
)
|
|
1162
|
-
assertWorkspaceParentSafety(baseRepo, profilePath)
|
|
1163
|
-
let profileMetadata
|
|
1164
|
-
try {
|
|
1165
|
-
profileMetadata = lstatSync(profilePath)
|
|
1166
|
-
} catch (error) {
|
|
1167
|
-
throw new RunnerUsageError(
|
|
1168
|
-
`Cannot read role profile ${role.profile}: ${errorMessage(error)}`,
|
|
1169
|
-
)
|
|
1170
|
-
}
|
|
1171
|
-
if (!profileMetadata.isFile() || profileMetadata.isSymbolicLink()) {
|
|
1172
|
-
throw new RunnerUsageError(
|
|
1173
|
-
`Role profile must be a regular non-symlink file: ${profilePath}`,
|
|
1174
|
-
)
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
if (role.workspace === 'current') {
|
|
1178
|
-
const executionCwd = path.join(executionRoot, role.key)
|
|
1179
|
-
ensureExecutionWorkspace(
|
|
1180
|
-
executionCwd,
|
|
1181
|
-
providerRoot,
|
|
1182
|
-
{
|
|
1183
|
-
run: wave.run,
|
|
1184
|
-
roleKey: role.key,
|
|
1185
|
-
repoDir: baseRepo,
|
|
1186
|
-
},
|
|
1187
|
-
options.platform ?? process.platform,
|
|
1188
|
-
)
|
|
1189
|
-
return {
|
|
1190
|
-
...role,
|
|
1191
|
-
cwd: executionCwd,
|
|
1192
|
-
repoDir: baseRepo,
|
|
1193
|
-
gitExcludeFile,
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
const worktreeId = role.workspace.slice('worktree:'.length)
|
|
1197
|
-
const target = path.join(worktreeRoot, worktreeId)
|
|
1198
|
-
const recorded = worktrees[worktreeId]
|
|
1199
|
-
if (recorded !== undefined && path.resolve(recorded) !== target) {
|
|
1200
|
-
throw new RunnerUsageError(
|
|
1201
|
-
`Role worktree mapping changed for ${worktreeId}`,
|
|
1202
|
-
)
|
|
1203
|
-
}
|
|
1204
|
-
const registeredTarget = existsSync(target)
|
|
1205
|
-
? existingPathIdentity(target)
|
|
1206
|
-
: path.resolve(target)
|
|
1207
|
-
if (!listed.has(registeredTarget)) {
|
|
1208
|
-
if (existsSync(target)) {
|
|
1209
|
-
throw new RunnerUsageError(
|
|
1210
|
-
`Unregistered path blocks role worktree ${worktreeId}: ${target}`,
|
|
1211
|
-
)
|
|
1212
|
-
}
|
|
1213
|
-
mkdirSync(path.dirname(target), { recursive: true })
|
|
1214
|
-
runGit(baseRepo, [
|
|
1215
|
-
'worktree',
|
|
1216
|
-
'add',
|
|
1217
|
-
'--detach',
|
|
1218
|
-
target,
|
|
1219
|
-
requireGitOid(baseCommit, 'role wave base commit'),
|
|
1220
|
-
])
|
|
1221
|
-
createdThisCall.push(target)
|
|
1222
|
-
}
|
|
1223
|
-
ensureProviderOverlay(
|
|
1224
|
-
providerRoot,
|
|
1225
|
-
target,
|
|
1226
|
-
options.platform ?? process.platform,
|
|
1227
|
-
)
|
|
1228
|
-
worktrees[worktreeId] = target
|
|
1229
|
-
return {
|
|
1230
|
-
...role,
|
|
1231
|
-
cwd: target,
|
|
1232
|
-
repoDir: target,
|
|
1233
|
-
gitExcludeFile,
|
|
1234
|
-
}
|
|
1235
|
-
})
|
|
1236
|
-
|
|
1237
|
-
writeRoleWaveManifest(manifestPath, {
|
|
1238
|
-
schemaVersion: 1,
|
|
1239
|
-
run: wave.run,
|
|
1240
|
-
baseRepo,
|
|
1241
|
-
baseCommit,
|
|
1242
|
-
sourceHead,
|
|
1243
|
-
worktrees,
|
|
1244
|
-
roles: {
|
|
1245
|
-
...(existingManifest?.roles ?? {}),
|
|
1246
|
-
...Object.fromEntries(
|
|
1247
|
-
roles.map((role) => [
|
|
1248
|
-
role.key,
|
|
1249
|
-
{
|
|
1250
|
-
executionCwd: role.cwd,
|
|
1251
|
-
repoDir: role.repoDir,
|
|
1252
|
-
workspace: role.workspace,
|
|
1253
|
-
gitExcludeFile: role.gitExcludeFile ?? null,
|
|
1254
|
-
},
|
|
1255
|
-
]),
|
|
1256
|
-
),
|
|
1257
|
-
},
|
|
1258
|
-
})
|
|
1259
|
-
return {
|
|
1260
|
-
run: wave.run,
|
|
1261
|
-
roles,
|
|
1262
|
-
baseRepo,
|
|
1263
|
-
baseCommit,
|
|
1264
|
-
manifestPath,
|
|
1265
|
-
}
|
|
1266
|
-
} catch (error) {
|
|
1267
|
-
for (const target of createdThisCall.reverse()) {
|
|
1268
|
-
try {
|
|
1269
|
-
runGit(baseRepo, ['worktree', 'remove', '--force', target])
|
|
1270
|
-
} catch {
|
|
1271
|
-
rmSync(target, { recursive: true, force: true })
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
if (createdBaseline) {
|
|
1275
|
-
try {
|
|
1276
|
-
runGit(
|
|
1277
|
-
baseRepo,
|
|
1278
|
-
['update-ref', '-d', `refs/specrails/kimi/${repoKey}/${wave.run}`],
|
|
1279
|
-
)
|
|
1280
|
-
} catch {
|
|
1281
|
-
// Preserve the original setup error. A stale private ref is harmless
|
|
1282
|
-
// and can be pruned by a later successful cleanup.
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
throw error
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function parseGitWorktreeList(source) {
|
|
1290
|
-
const paths = new Set()
|
|
1291
|
-
for (const field of source.split('\0')) {
|
|
1292
|
-
if (field.startsWith('worktree ')) {
|
|
1293
|
-
const candidate = field.slice('worktree '.length)
|
|
1294
|
-
paths.add(
|
|
1295
|
-
existsSync(candidate)
|
|
1296
|
-
? existingPathIdentity(candidate)
|
|
1297
|
-
: path.resolve(candidate),
|
|
1298
|
-
)
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
return paths
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
function createSyntheticBaselineCommit({
|
|
1305
|
-
baseRepo,
|
|
1306
|
-
head,
|
|
1307
|
-
repoKey,
|
|
1308
|
-
run,
|
|
1309
|
-
tempRoot,
|
|
1310
|
-
runGit,
|
|
1311
|
-
}) {
|
|
1312
|
-
mkdirSync(tempRoot, { recursive: true })
|
|
1313
|
-
const indexDirectory = path.join(
|
|
1314
|
-
tempRoot,
|
|
1315
|
-
'specrails-kimi-indexes',
|
|
1316
|
-
repoKey,
|
|
1317
|
-
)
|
|
1318
|
-
ensureRealDirectoryTree(tempRoot, indexDirectory)
|
|
1319
|
-
const indexPath = path.join(indexDirectory, `${run}.index`)
|
|
1320
|
-
if (existsSync(indexPath)) {
|
|
1321
|
-
const metadata = lstatSync(indexPath)
|
|
1322
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
1323
|
-
throw new RunnerUsageError(
|
|
1324
|
-
`Invalid synthetic baseline index: ${indexPath}`,
|
|
1325
|
-
)
|
|
1326
|
-
}
|
|
1327
|
-
unlinkSync(indexPath)
|
|
1328
|
-
}
|
|
1329
|
-
const indexEnv = { GIT_INDEX_FILE: indexPath }
|
|
1330
|
-
try {
|
|
1331
|
-
runGit(baseRepo, ['read-tree', head], undefined, indexEnv)
|
|
1332
|
-
runGit(
|
|
1333
|
-
baseRepo,
|
|
1334
|
-
[
|
|
1335
|
-
'add',
|
|
1336
|
-
'-A',
|
|
1337
|
-
'--',
|
|
1338
|
-
'.',
|
|
1339
|
-
':(exclude).kimi-code',
|
|
1340
|
-
':(exclude).kimi-code/**',
|
|
1341
|
-
`:(exclude)${ROLE_WAVE_RELATIVE_PATH.split(path.sep).join('/')}`,
|
|
1342
|
-
`:(exclude)${ROLE_REQUEST_RELATIVE_PATH.split(path.sep).join('/')}`,
|
|
1343
|
-
`:(exclude)${ROLE_MERGE_RELATIVE_PATH.split(path.sep).join('/')}`,
|
|
1344
|
-
`:(exclude)${ROLE_WORKTREE_MANIFEST_DIR.split(path.sep).join('/')}/**`,
|
|
1345
|
-
],
|
|
1346
|
-
undefined,
|
|
1347
|
-
indexEnv,
|
|
1348
|
-
)
|
|
1349
|
-
const tree = requireGitOid(
|
|
1350
|
-
runGit(baseRepo, ['write-tree'], undefined, indexEnv).trim(),
|
|
1351
|
-
'synthetic baseline tree',
|
|
1352
|
-
)
|
|
1353
|
-
const identityEnv = {
|
|
1354
|
-
...indexEnv,
|
|
1355
|
-
GIT_AUTHOR_NAME: 'SpecRails Kimi',
|
|
1356
|
-
GIT_AUTHOR_EMAIL: 'kimi-baseline@specrails.local',
|
|
1357
|
-
GIT_COMMITTER_NAME: 'SpecRails Kimi',
|
|
1358
|
-
GIT_COMMITTER_EMAIL: 'kimi-baseline@specrails.local',
|
|
1359
|
-
}
|
|
1360
|
-
const commit = requireGitOid(
|
|
1361
|
-
runGit(
|
|
1362
|
-
baseRepo,
|
|
1363
|
-
['commit-tree', tree, '-p', head],
|
|
1364
|
-
`SpecRails Kimi baseline ${run}\n`,
|
|
1365
|
-
identityEnv,
|
|
1366
|
-
).trim(),
|
|
1367
|
-
'synthetic baseline commit',
|
|
1368
|
-
)
|
|
1369
|
-
runGit(
|
|
1370
|
-
baseRepo,
|
|
1371
|
-
['update-ref', `refs/specrails/kimi/${repoKey}/${run}`, commit],
|
|
1372
|
-
)
|
|
1373
|
-
return commit
|
|
1374
|
-
} finally {
|
|
1375
|
-
rmSync(indexPath, { force: true })
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
function writeRoleGitExclude(file) {
|
|
1380
|
-
const contents = roleGitExcludeContents()
|
|
1381
|
-
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
1382
|
-
if (existsSync(file)) {
|
|
1383
|
-
const metadata = lstatSync(file)
|
|
1384
|
-
if (
|
|
1385
|
-
!metadata.isFile() ||
|
|
1386
|
-
metadata.isSymbolicLink() ||
|
|
1387
|
-
readFileSync(file, 'utf8') !== contents
|
|
1388
|
-
) {
|
|
1389
|
-
throw new RunnerUsageError(`Invalid role git exclude file: ${file}`)
|
|
1390
|
-
}
|
|
1391
|
-
return file
|
|
1392
|
-
}
|
|
1393
|
-
writeFileSync(file, contents, {
|
|
1394
|
-
encoding: 'utf8',
|
|
1395
|
-
flag: 'wx',
|
|
1396
|
-
mode: 0o600,
|
|
1397
|
-
})
|
|
1398
|
-
return file
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
function roleGitExcludeContents() {
|
|
1402
|
-
return [
|
|
1403
|
-
'/.kimi-code',
|
|
1404
|
-
'/.kimi-code/',
|
|
1405
|
-
'/.specrails/kimi-role-wave.json',
|
|
1406
|
-
'/.specrails/kimi-role-request.json',
|
|
1407
|
-
'/.specrails/kimi-role-merge.json',
|
|
1408
|
-
'/.specrails/kimi-role-worktrees/',
|
|
1409
|
-
'',
|
|
1410
|
-
].join('\n')
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
function safeWorkspacePath(root, relative) {
|
|
1414
|
-
if (path.isAbsolute(relative) || relative.includes('\0')) {
|
|
1415
|
-
throw new RunnerUsageError(`Unsafe git workspace path: ${relative}`)
|
|
1416
|
-
}
|
|
1417
|
-
const resolved = path.resolve(root, relative)
|
|
1418
|
-
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`
|
|
1419
|
-
if (!resolved.startsWith(prefix)) {
|
|
1420
|
-
throw new RunnerUsageError(`Git workspace path escapes its root: ${relative}`)
|
|
1421
|
-
}
|
|
1422
|
-
return resolved
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
function isManagedRolePath(relative) {
|
|
1426
|
-
return (
|
|
1427
|
-
relative === '.kimi-code' ||
|
|
1428
|
-
relative.startsWith('.kimi-code/') ||
|
|
1429
|
-
relative === '.specrails/kimi-role-wave.json' ||
|
|
1430
|
-
relative === '.specrails/kimi-role-request.json' ||
|
|
1431
|
-
relative === '.specrails/kimi-role-merge.json' ||
|
|
1432
|
-
relative.startsWith('.specrails/kimi-role-worktrees/')
|
|
1433
|
-
)
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function assertManagedPathParents(root, file, label) {
|
|
1437
|
-
const absoluteRoot = path.resolve(root)
|
|
1438
|
-
const parent = path.dirname(path.resolve(file))
|
|
1439
|
-
const relative = path.relative(absoluteRoot, parent)
|
|
1440
|
-
if (
|
|
1441
|
-
relative === '..' ||
|
|
1442
|
-
relative.startsWith(`..${path.sep}`) ||
|
|
1443
|
-
path.isAbsolute(relative)
|
|
1444
|
-
) {
|
|
1445
|
-
throw new RunnerUsageError(`${label} escapes its working directory`)
|
|
1446
|
-
}
|
|
1447
|
-
let cursor = absoluteRoot
|
|
1448
|
-
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
1449
|
-
cursor = path.join(cursor, component)
|
|
1450
|
-
let metadata
|
|
1451
|
-
try {
|
|
1452
|
-
metadata = lstatSync(cursor)
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
throw new RunnerUsageError(
|
|
1455
|
-
`Cannot inspect ${label} parent ${cursor}: ${errorMessage(error)}`,
|
|
1456
|
-
)
|
|
1457
|
-
}
|
|
1458
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1459
|
-
throw new RunnerUsageError(
|
|
1460
|
-
`${label} parent must be a real directory: ${cursor}`,
|
|
1461
|
-
)
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function ensureRealDirectoryTree(root, directory) {
|
|
1467
|
-
const lexicalRoot = path.resolve(root)
|
|
1468
|
-
const absoluteRoot = canonicalRealpath(lexicalRoot)
|
|
1469
|
-
const lexicalTarget = path.resolve(directory)
|
|
1470
|
-
const relative = path.relative(lexicalRoot, lexicalTarget)
|
|
1471
|
-
if (
|
|
1472
|
-
relative === '..' ||
|
|
1473
|
-
relative.startsWith(`..${path.sep}`) ||
|
|
1474
|
-
path.isAbsolute(relative)
|
|
1475
|
-
) {
|
|
1476
|
-
throw new RunnerUsageError(
|
|
1477
|
-
`Managed directory escapes its root: ${directory}`,
|
|
1478
|
-
)
|
|
1479
|
-
}
|
|
1480
|
-
let cursor = absoluteRoot
|
|
1481
|
-
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
1482
|
-
cursor = path.join(cursor, component)
|
|
1483
|
-
if (!existsSync(cursor)) {
|
|
1484
|
-
mkdirSync(cursor, { mode: 0o700 })
|
|
1485
|
-
continue
|
|
1486
|
-
}
|
|
1487
|
-
const metadata = lstatSync(cursor)
|
|
1488
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1489
|
-
throw new RunnerUsageError(
|
|
1490
|
-
`Managed directory component must not be a symlink: ${cursor}`,
|
|
1491
|
-
)
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function ensureManagedTempRoot(candidate) {
|
|
1497
|
-
const absolute = path.resolve(candidate)
|
|
1498
|
-
if (existsSync(absolute)) {
|
|
1499
|
-
const metadata = lstatSync(absolute)
|
|
1500
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1501
|
-
throw new RunnerUsageError(
|
|
1502
|
-
`Managed temp root must be a real directory: ${absolute}`,
|
|
1503
|
-
)
|
|
1504
|
-
}
|
|
1505
|
-
return canonicalRealpath(absolute)
|
|
1506
|
-
}
|
|
1507
|
-
const parent = path.dirname(absolute)
|
|
1508
|
-
const realParent = canonicalRealpath(parent)
|
|
1509
|
-
ensureRealDirectoryTree(realParent, path.join(realParent, path.basename(absolute)))
|
|
1510
|
-
return canonicalRealpath(path.join(realParent, path.basename(absolute)))
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
export function ensureProviderOverlay(
|
|
1514
|
-
providerRoot,
|
|
1515
|
-
workspace,
|
|
1516
|
-
platform,
|
|
1517
|
-
dependencies = {},
|
|
1518
|
-
) {
|
|
1519
|
-
const createLink = dependencies.symlink ?? symlinkSync
|
|
1520
|
-
const copyTree = dependencies.copyTree ?? cpSync
|
|
1521
|
-
const removeTree = dependencies.removeTree ?? rmSync
|
|
1522
|
-
const expectedRoot = canonicalRealpath(providerRoot)
|
|
1523
|
-
const contentSha256 = hashManagedDirectoryTree(expectedRoot)
|
|
1524
|
-
const markerValue = {
|
|
1525
|
-
schemaVersion: 2,
|
|
1526
|
-
providerRoot: expectedRoot,
|
|
1527
|
-
runnerSha256: createHash('sha256')
|
|
1528
|
-
.update(readFileSync(path.join(expectedRoot, 'specrails', 'run-skill.mjs')))
|
|
1529
|
-
.digest('hex'),
|
|
1530
|
-
contentSha256,
|
|
1531
|
-
}
|
|
1532
|
-
const destination = path.join(workspace, '.kimi-code')
|
|
1533
|
-
if (existsSync(destination)) {
|
|
1534
|
-
const destinationMetadata = lstatSync(destination)
|
|
1535
|
-
if (destinationMetadata.isSymbolicLink()) {
|
|
1536
|
-
if (
|
|
1537
|
-
existingPathIdentity(destination) !==
|
|
1538
|
-
existingPathIdentity(expectedRoot)
|
|
1539
|
-
) {
|
|
1540
|
-
throw new RunnerUsageError(
|
|
1541
|
-
`Worktree provider link does not target the managed provider: ${destination}`,
|
|
1542
|
-
)
|
|
1543
|
-
}
|
|
1544
|
-
return
|
|
1545
|
-
}
|
|
1546
|
-
if (!destinationMetadata.isDirectory()) {
|
|
1547
|
-
throw new RunnerUsageError(
|
|
1548
|
-
`Worktree provider path is not a directory: ${destination}`,
|
|
1549
|
-
)
|
|
1550
|
-
}
|
|
1551
|
-
const markerPath = path.join(
|
|
1552
|
-
destination,
|
|
1553
|
-
'.specrails-managed-overlay.json',
|
|
1554
|
-
)
|
|
1555
|
-
if (!existsSync(markerPath) || lstatSync(markerPath).isSymbolicLink()) {
|
|
1556
|
-
throw new RunnerUsageError(
|
|
1557
|
-
`Refusing unverified worktree provider directory: ${destination}`,
|
|
1558
|
-
)
|
|
1559
|
-
}
|
|
1560
|
-
let marker
|
|
1561
|
-
try {
|
|
1562
|
-
marker = JSON.parse(readFileSync(markerPath, 'utf8'))
|
|
1563
|
-
} catch (error) {
|
|
1564
|
-
throw new RunnerUsageError(
|
|
1565
|
-
`Cannot verify worktree provider overlay: ${errorMessage(error)}`,
|
|
1566
|
-
)
|
|
1567
|
-
}
|
|
1568
|
-
const isOwnedMarker =
|
|
1569
|
-
isRecord(marker) &&
|
|
1570
|
-
(marker.schemaVersion === 1 || marker.schemaVersion === 2) &&
|
|
1571
|
-
marker.providerRoot === expectedRoot &&
|
|
1572
|
-
typeof marker.runnerSha256 === 'string' &&
|
|
1573
|
-
/^[0-9a-f]{64}$/.test(marker.runnerSha256) &&
|
|
1574
|
-
(
|
|
1575
|
-
marker.schemaVersion === 1 ||
|
|
1576
|
-
(
|
|
1577
|
-
typeof marker.contentSha256 === 'string' &&
|
|
1578
|
-
/^[0-9a-f]{64}$/.test(marker.contentSha256)
|
|
1579
|
-
)
|
|
1580
|
-
)
|
|
1581
|
-
if (!isOwnedMarker) {
|
|
1582
|
-
throw new RunnerUsageError(
|
|
1583
|
-
`Worktree provider overlay does not match the managed provider: ${destination}`,
|
|
1584
|
-
)
|
|
1585
|
-
}
|
|
1586
|
-
const actualContentSha256 = hashManagedDirectoryTree(destination, {
|
|
1587
|
-
ignoredRelativePath: '.specrails-managed-overlay.json',
|
|
1588
|
-
})
|
|
1589
|
-
if (
|
|
1590
|
-
JSON.stringify(marker) === JSON.stringify(markerValue) &&
|
|
1591
|
-
actualContentSha256 === contentSha256
|
|
1592
|
-
) {
|
|
1593
|
-
return
|
|
1594
|
-
}
|
|
1595
|
-
// A valid ownership marker lets us repair stale/corrupt copied overlays.
|
|
1596
|
-
// Unmarked directories remain user-owned and are never removed.
|
|
1597
|
-
removeTree(destination, { recursive: true, force: true })
|
|
1598
|
-
}
|
|
1599
|
-
try {
|
|
1600
|
-
createLink(
|
|
1601
|
-
expectedRoot,
|
|
1602
|
-
destination,
|
|
1603
|
-
platform === 'win32' ? 'junction' : 'dir',
|
|
1604
|
-
)
|
|
1605
|
-
} catch {
|
|
1606
|
-
removeTree(destination, { recursive: true, force: true })
|
|
1607
|
-
copyTree(providerRoot, destination, {
|
|
1608
|
-
recursive: true,
|
|
1609
|
-
dereference: true,
|
|
1610
|
-
errorOnExist: true,
|
|
1611
|
-
})
|
|
1612
|
-
const copiedContentSha256 = hashManagedDirectoryTree(destination)
|
|
1613
|
-
if (copiedContentSha256 !== contentSha256) {
|
|
1614
|
-
removeTree(destination, { recursive: true, force: true })
|
|
1615
|
-
throw new RunnerUsageError(
|
|
1616
|
-
`Copied worktree provider overlay failed content verification: ${destination}`,
|
|
1617
|
-
)
|
|
1618
|
-
}
|
|
1619
|
-
writeFileSync(
|
|
1620
|
-
path.join(destination, '.specrails-managed-overlay.json'),
|
|
1621
|
-
`${JSON.stringify(markerValue)}\n`,
|
|
1622
|
-
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
|
1623
|
-
)
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
function hashManagedDirectoryTree(
|
|
1628
|
-
root,
|
|
1629
|
-
options = { ignoredRelativePath: undefined },
|
|
1630
|
-
) {
|
|
1631
|
-
const hash = createHash('sha256')
|
|
1632
|
-
const walk = (directory, prefix) => {
|
|
1633
|
-
const names = readdirSync(directory).sort()
|
|
1634
|
-
for (const name of names) {
|
|
1635
|
-
const relative = prefix === '' ? name : `${prefix}/${name}`
|
|
1636
|
-
if (relative === options.ignoredRelativePath) continue
|
|
1637
|
-
const entry = path.join(directory, name)
|
|
1638
|
-
const metadata = lstatSync(entry)
|
|
1639
|
-
if (metadata.isSymbolicLink()) {
|
|
1640
|
-
throw new RunnerUsageError(
|
|
1641
|
-
`Managed provider tree must not contain symlinks: ${entry}`,
|
|
1642
|
-
)
|
|
1643
|
-
}
|
|
1644
|
-
if (metadata.isDirectory()) {
|
|
1645
|
-
hash.update(`D\0${relative}\0`)
|
|
1646
|
-
walk(entry, relative)
|
|
1647
|
-
} else if (metadata.isFile()) {
|
|
1648
|
-
hash.update(`F\0${relative}\0${metadata.mode & 0o111}\0`)
|
|
1649
|
-
hash.update(readFileSync(entry))
|
|
1650
|
-
hash.update('\0')
|
|
1651
|
-
} else {
|
|
1652
|
-
throw new RunnerUsageError(
|
|
1653
|
-
`Managed provider tree has unsupported entry: ${entry}`,
|
|
1654
|
-
)
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
walk(root, '')
|
|
1659
|
-
return hash.digest('hex')
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function ensureExecutionWorkspace(
|
|
1663
|
-
executionCwd,
|
|
1664
|
-
providerRoot,
|
|
1665
|
-
identity,
|
|
1666
|
-
platform,
|
|
1667
|
-
) {
|
|
1668
|
-
mkdirSync(executionCwd, { recursive: true, mode: 0o700 })
|
|
1669
|
-
const metadata = lstatSync(executionCwd)
|
|
1670
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1671
|
-
throw new RunnerUsageError(
|
|
1672
|
-
`Role execution workspace must be a real directory: ${executionCwd}`,
|
|
1673
|
-
)
|
|
1674
|
-
}
|
|
1675
|
-
const marker = path.join(executionCwd, '.specrails-role-workspace.json')
|
|
1676
|
-
if (existsSync(marker)) {
|
|
1677
|
-
const markerMetadata = lstatSync(marker)
|
|
1678
|
-
if (!markerMetadata.isFile() || markerMetadata.isSymbolicLink()) {
|
|
1679
|
-
throw new RunnerUsageError(
|
|
1680
|
-
`Invalid role execution workspace marker: ${marker}`,
|
|
1681
|
-
)
|
|
1682
|
-
}
|
|
1683
|
-
let existing
|
|
1684
|
-
try {
|
|
1685
|
-
existing = JSON.parse(readFileSync(marker, 'utf8'))
|
|
1686
|
-
} catch (error) {
|
|
1687
|
-
throw new RunnerUsageError(
|
|
1688
|
-
`Cannot parse role execution workspace marker: ${errorMessage(error)}`,
|
|
1689
|
-
)
|
|
1690
|
-
}
|
|
1691
|
-
if (JSON.stringify(existing) !== JSON.stringify(identity)) {
|
|
1692
|
-
throw new RunnerUsageError(
|
|
1693
|
-
`Role execution workspace belongs to another run: ${executionCwd}`,
|
|
1694
|
-
)
|
|
1695
|
-
}
|
|
1696
|
-
} else {
|
|
1697
|
-
writeFileSync(marker, `${JSON.stringify(identity)}\n`, {
|
|
1698
|
-
encoding: 'utf8',
|
|
1699
|
-
flag: 'wx',
|
|
1700
|
-
mode: 0o600,
|
|
1701
|
-
})
|
|
1702
|
-
}
|
|
1703
|
-
ensureProviderOverlay(providerRoot, executionCwd, platform)
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
function readRoleWaveManifest(manifestPath) {
|
|
1707
|
-
if (!existsSync(manifestPath)) return undefined
|
|
1708
|
-
const metadata = lstatSync(manifestPath)
|
|
1709
|
-
if (
|
|
1710
|
-
!metadata.isFile() ||
|
|
1711
|
-
metadata.isSymbolicLink() ||
|
|
1712
|
-
metadata.size > MAX_ROLE_REQUEST_BYTES
|
|
1713
|
-
) {
|
|
1714
|
-
throw new RunnerUsageError(
|
|
1715
|
-
`Invalid role wave manifest: ${manifestPath}`,
|
|
1716
|
-
)
|
|
1717
|
-
}
|
|
1718
|
-
let value
|
|
1719
|
-
try {
|
|
1720
|
-
value = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
1721
|
-
} catch (error) {
|
|
1722
|
-
throw new RunnerUsageError(
|
|
1723
|
-
`Cannot parse role wave manifest ${manifestPath}: ${errorMessage(error)}`,
|
|
1724
|
-
)
|
|
1725
|
-
}
|
|
1726
|
-
if (!isRecord(value)) {
|
|
1727
|
-
throw new RunnerUsageError(`Invalid role wave manifest: ${manifestPath}`)
|
|
1728
|
-
}
|
|
1729
|
-
const keys = Object.keys(value).sort()
|
|
1730
|
-
if (
|
|
1731
|
-
keys.join(',') !==
|
|
1732
|
-
'baseCommit,baseRepo,roles,run,schemaVersion,sourceHead,worktrees' ||
|
|
1733
|
-
value.schemaVersion !== 1 ||
|
|
1734
|
-
typeof value.baseRepo !== 'string' ||
|
|
1735
|
-
!(
|
|
1736
|
-
value.baseCommit === null ||
|
|
1737
|
-
(
|
|
1738
|
-
typeof value.baseCommit === 'string' &&
|
|
1739
|
-
SAFE_GIT_OID.test(value.baseCommit)
|
|
1740
|
-
)
|
|
1741
|
-
) ||
|
|
1742
|
-
!(
|
|
1743
|
-
value.sourceHead === null ||
|
|
1744
|
-
(
|
|
1745
|
-
typeof value.sourceHead === 'string' &&
|
|
1746
|
-
SAFE_GIT_OID.test(value.sourceHead)
|
|
1747
|
-
)
|
|
1748
|
-
) ||
|
|
1749
|
-
!SAFE_WAVE_ID.test(value.run) ||
|
|
1750
|
-
!isRecord(value.worktrees) ||
|
|
1751
|
-
!isRecord(value.roles)
|
|
1752
|
-
) {
|
|
1753
|
-
throw new RunnerUsageError(`Invalid role wave manifest: ${manifestPath}`)
|
|
1754
|
-
}
|
|
1755
|
-
const worktrees = {}
|
|
1756
|
-
for (const [key, target] of Object.entries(value.worktrees)) {
|
|
1757
|
-
requireSafeWaveId(key, 'manifest worktree id')
|
|
1758
|
-
if (typeof target !== 'string' || !path.isAbsolute(target)) {
|
|
1759
|
-
throw new RunnerUsageError(`Invalid role wave manifest: ${manifestPath}`)
|
|
1760
|
-
}
|
|
1761
|
-
worktrees[key] = target
|
|
1762
|
-
}
|
|
1763
|
-
const roles = {}
|
|
1764
|
-
for (const [key, role] of Object.entries(value.roles)) {
|
|
1765
|
-
requireSafeWaveId(key, 'manifest role key')
|
|
1766
|
-
if (
|
|
1767
|
-
!isRecord(role) ||
|
|
1768
|
-
Object.keys(role).sort().join(',') !==
|
|
1769
|
-
'executionCwd,gitExcludeFile,repoDir,workspace' ||
|
|
1770
|
-
typeof role.executionCwd !== 'string' ||
|
|
1771
|
-
!path.isAbsolute(role.executionCwd) ||
|
|
1772
|
-
typeof role.repoDir !== 'string' ||
|
|
1773
|
-
!path.isAbsolute(role.repoDir) ||
|
|
1774
|
-
!(
|
|
1775
|
-
role.gitExcludeFile === null ||
|
|
1776
|
-
(
|
|
1777
|
-
typeof role.gitExcludeFile === 'string' &&
|
|
1778
|
-
path.isAbsolute(role.gitExcludeFile)
|
|
1779
|
-
)
|
|
1780
|
-
) ||
|
|
1781
|
-
typeof role.workspace !== 'string'
|
|
1782
|
-
) {
|
|
1783
|
-
throw new RunnerUsageError(`Invalid role wave manifest: ${manifestPath}`)
|
|
1784
|
-
}
|
|
1785
|
-
roles[key] = role
|
|
1786
|
-
}
|
|
1787
|
-
return { ...value, worktrees, roles }
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
function writeRoleWaveManifest(manifestPath, value) {
|
|
1791
|
-
const directory = path.dirname(manifestPath)
|
|
1792
|
-
ensureRealDirectoryTree(value.baseRepo, directory)
|
|
1793
|
-
const metadata = lstatSync(directory)
|
|
1794
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1795
|
-
throw new RunnerUsageError(
|
|
1796
|
-
`Role worktree manifest directory must not be a symlink: ${directory}`,
|
|
1797
|
-
)
|
|
1798
|
-
}
|
|
1799
|
-
if (existsSync(manifestPath) && lstatSync(manifestPath).isSymbolicLink()) {
|
|
1800
|
-
throw new RunnerUsageError(
|
|
1801
|
-
`Role wave manifest must not be a symlink: ${manifestPath}`,
|
|
1802
|
-
)
|
|
1803
|
-
}
|
|
1804
|
-
const temporary = `${manifestPath}.${process.pid}.${Date.now()}.tmp`
|
|
1805
|
-
try {
|
|
1806
|
-
writeFileSync(
|
|
1807
|
-
temporary,
|
|
1808
|
-
`${JSON.stringify(value, null, 2)}\n`,
|
|
1809
|
-
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
|
1810
|
-
)
|
|
1811
|
-
renameSync(temporary, manifestPath)
|
|
1812
|
-
} finally {
|
|
1813
|
-
rmSync(temporary, { force: true })
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
export function inspectRoleWaveStatus(run, dependencies = {}) {
|
|
1818
|
-
const state = resolveRoleWaveState(run, dependencies)
|
|
1819
|
-
const worktrees = {}
|
|
1820
|
-
for (const [worktree, repoDir] of Object.entries(state.manifest.worktrees)) {
|
|
1821
|
-
const changes = new Map()
|
|
1822
|
-
const tracked = runRoleGit(
|
|
1823
|
-
repoDir,
|
|
1824
|
-
[
|
|
1825
|
-
'diff',
|
|
1826
|
-
'--name-status',
|
|
1827
|
-
'-z',
|
|
1828
|
-
'--no-renames',
|
|
1829
|
-
state.manifest.baseCommit,
|
|
1830
|
-
'--',
|
|
1831
|
-
],
|
|
1832
|
-
dependencies,
|
|
1833
|
-
).split('\0')
|
|
1834
|
-
for (let index = 0; index + 1 < tracked.length; index += 2) {
|
|
1835
|
-
const rawStatus = tracked[index]
|
|
1836
|
-
const relative = tracked[index + 1]
|
|
1837
|
-
if (!rawStatus || !relative || isManagedRolePath(relative)) continue
|
|
1838
|
-
const status =
|
|
1839
|
-
rawStatus.startsWith('A')
|
|
1840
|
-
? 'A'
|
|
1841
|
-
: rawStatus.startsWith('D')
|
|
1842
|
-
? 'D'
|
|
1843
|
-
: 'M'
|
|
1844
|
-
changes.set(relative, { status, path: relative })
|
|
1845
|
-
}
|
|
1846
|
-
const untracked = runRoleGit(
|
|
1847
|
-
repoDir,
|
|
1848
|
-
['ls-files', '--others', '--exclude-standard', '-z'],
|
|
1849
|
-
dependencies,
|
|
1850
|
-
).split('\0')
|
|
1851
|
-
for (const relative of untracked) {
|
|
1852
|
-
if (
|
|
1853
|
-
relative === '' ||
|
|
1854
|
-
isManagedRolePath(relative) ||
|
|
1855
|
-
changes.has(relative)
|
|
1856
|
-
) {
|
|
1857
|
-
continue
|
|
1858
|
-
}
|
|
1859
|
-
changes.set(relative, { status: 'A', path: relative })
|
|
1860
|
-
}
|
|
1861
|
-
worktrees[worktree] = {
|
|
1862
|
-
repoDir,
|
|
1863
|
-
changes: Array.from(changes.values()).sort((left, right) =>
|
|
1864
|
-
left.path.localeCompare(right.path),
|
|
1865
|
-
),
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
return {
|
|
1869
|
-
run,
|
|
1870
|
-
baseRepo: state.baseRepo,
|
|
1871
|
-
baseCommit: state.manifest.baseCommit,
|
|
1872
|
-
manifestPath: state.manifestPath,
|
|
1873
|
-
worktrees,
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
export function applyRoleMerge(merge, dependencies = {}) {
|
|
1878
|
-
const state = resolveRoleWaveState(merge.run, dependencies)
|
|
1879
|
-
const prepared = merge.actions.map((action) => {
|
|
1880
|
-
const workspace = state.manifest.worktrees[action.worktree]
|
|
1881
|
-
if (workspace === undefined) {
|
|
1882
|
-
throw new RunnerUsageError(
|
|
1883
|
-
`Unknown role merge worktree: ${action.worktree}`,
|
|
456
|
+
`Attachment path must be absolute: ${attachment}`,
|
|
1884
457
|
)
|
|
1885
458
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
try {
|
|
1891
|
-
metadata = lstatSync(source)
|
|
1892
|
-
} catch (error) {
|
|
1893
|
-
throw new RunnerUsageError(
|
|
1894
|
-
`Cannot copy missing role output ${action.path}: ${errorMessage(error)}`,
|
|
1895
|
-
)
|
|
1896
|
-
}
|
|
1897
|
-
if (!metadata.isFile() && !metadata.isSymbolicLink()) {
|
|
1898
|
-
throw new RunnerUsageError(
|
|
1899
|
-
`Role merge copy source must be a file or symlink: ${action.path}`,
|
|
1900
|
-
)
|
|
459
|
+
try {
|
|
460
|
+
const metadata = inspect(attachment)
|
|
461
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
462
|
+
throw new Error('not a regular non-symlink file')
|
|
1901
463
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
464
|
+
const canonical = resolvePath(attachment)
|
|
465
|
+
assertReadable(canonical)
|
|
466
|
+
return canonical
|
|
467
|
+
} catch (error) {
|
|
1906
468
|
throw new RunnerUsageError(
|
|
1907
|
-
`
|
|
469
|
+
`Attachment must be a readable regular non-symlink file: ` +
|
|
470
|
+
`${attachment}: ${errorMessage(error)}`,
|
|
1908
471
|
)
|
|
1909
472
|
}
|
|
1910
|
-
assertWorkspaceParentSafety(state.baseRepo, target)
|
|
1911
|
-
return { ...action, source, target }
|
|
1912
473
|
})
|
|
1913
|
-
|
|
1914
|
-
for (const action of prepared) {
|
|
1915
|
-
ensureWorkspaceParentDirectories(state.baseRepo, action.target)
|
|
1916
|
-
if (action.operation === 'delete') {
|
|
1917
|
-
if (existsSync(action.target) || lstatExists(action.target)) {
|
|
1918
|
-
const metadata = lstatSync(action.target)
|
|
1919
|
-
if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
|
|
1920
|
-
throw new RunnerUsageError(
|
|
1921
|
-
`Role merge refuses to delete directory path: ${action.path}`,
|
|
1922
|
-
)
|
|
1923
|
-
}
|
|
1924
|
-
unlinkSync(action.target)
|
|
1925
|
-
}
|
|
1926
|
-
continue
|
|
1927
|
-
}
|
|
1928
|
-
const existing = lstatExists(action.target)
|
|
1929
|
-
? lstatSync(action.target)
|
|
1930
|
-
: undefined
|
|
1931
|
-
if (existing?.isDirectory() && !existing.isSymbolicLink()) {
|
|
1932
|
-
throw new RunnerUsageError(
|
|
1933
|
-
`Role merge refuses to replace directory path: ${action.path}`,
|
|
1934
|
-
)
|
|
1935
|
-
}
|
|
1936
|
-
if (action.metadata.isSymbolicLink()) {
|
|
1937
|
-
if (existing) unlinkSync(action.target)
|
|
1938
|
-
symlinkSync(readlinkSync(action.source), action.target)
|
|
1939
|
-
continue
|
|
1940
|
-
}
|
|
1941
|
-
const temporary = path.join(
|
|
1942
|
-
path.dirname(action.target),
|
|
1943
|
-
`.${path.basename(action.target)}.specrails-${process.pid}.tmp`,
|
|
1944
|
-
)
|
|
1945
|
-
try {
|
|
1946
|
-
copyFileSync(action.source, temporary)
|
|
1947
|
-
chmodSync(temporary, action.metadata.mode & 0o777)
|
|
1948
|
-
if (existing) unlinkSync(action.target)
|
|
1949
|
-
renameSync(temporary, action.target)
|
|
1950
|
-
} finally {
|
|
1951
|
-
rmSync(temporary, { force: true })
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
return {
|
|
1955
|
-
run: merge.run,
|
|
1956
|
-
baseRepo: state.baseRepo,
|
|
1957
|
-
applied: prepared.length,
|
|
1958
|
-
}
|
|
1959
474
|
}
|
|
1960
475
|
|
|
1961
|
-
export function
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
})
|
|
1965
|
-
const repoKey = state.repoKey
|
|
1966
|
-
const failures = []
|
|
1967
|
-
for (const [worktree, target] of Object.entries(state.manifest.worktrees)) {
|
|
1968
|
-
try {
|
|
1969
|
-
runRoleGit(
|
|
1970
|
-
state.baseRepo,
|
|
1971
|
-
['worktree', 'remove', '--force', target],
|
|
1972
|
-
dependencies,
|
|
1973
|
-
)
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
failures.push(`${worktree}: ${errorMessage(error)}`)
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
if (failures.length > 0) {
|
|
1979
|
-
throw new RunnerUsageError(
|
|
1980
|
-
`Role wave cleanup failed; manifest retained: ${failures.join('; ')}`,
|
|
1981
|
-
)
|
|
1982
|
-
}
|
|
1983
|
-
if (state.manifest.baseCommit !== null) {
|
|
1984
|
-
runRoleGit(
|
|
1985
|
-
state.baseRepo,
|
|
1986
|
-
['update-ref', '-d', `refs/specrails/kimi/${repoKey}/${run}`],
|
|
1987
|
-
dependencies,
|
|
1988
|
-
)
|
|
1989
|
-
}
|
|
1990
|
-
unlinkSync(state.manifestPath)
|
|
1991
|
-
|
|
1992
|
-
const tempRoot = state.tempRoot
|
|
1993
|
-
for (const directory of [
|
|
1994
|
-
path.join(
|
|
1995
|
-
tempRoot,
|
|
1996
|
-
'specrails-kimi-worktrees',
|
|
1997
|
-
repoKey,
|
|
1998
|
-
run,
|
|
1999
|
-
),
|
|
2000
|
-
path.join(
|
|
2001
|
-
tempRoot,
|
|
2002
|
-
'specrails-kimi-execution',
|
|
2003
|
-
repoKey,
|
|
2004
|
-
run,
|
|
2005
|
-
),
|
|
2006
|
-
]) {
|
|
2007
|
-
rmSync(directory, { recursive: true, force: true })
|
|
2008
|
-
}
|
|
2009
|
-
rmSync(
|
|
2010
|
-
path.join(
|
|
2011
|
-
tempRoot,
|
|
2012
|
-
'specrails-kimi-git-excludes',
|
|
2013
|
-
repoKey,
|
|
2014
|
-
`${run}.txt`,
|
|
2015
|
-
),
|
|
2016
|
-
{ force: true },
|
|
476
|
+
export function parseNpmCmdShimEntry(shimPath, contents) {
|
|
477
|
+
const match = contents.match(
|
|
478
|
+
/%dp0%[\\/]([^"\r\n]*?\.(?:mjs|cjs|js))["']?\s+%\*/i,
|
|
2017
479
|
)
|
|
2018
|
-
|
|
2019
|
-
return
|
|
2020
|
-
run,
|
|
2021
|
-
baseRepo: state.baseRepo,
|
|
2022
|
-
removedWorktrees: Object.keys(state.manifest.worktrees).length,
|
|
2023
|
-
manifestPath: state.manifestPath,
|
|
2024
|
-
}
|
|
480
|
+
if (!match?.[1]) return null
|
|
481
|
+
return path.win32.join(path.win32.dirname(shimPath), match[1])
|
|
2025
482
|
}
|
|
2026
483
|
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
dependencies.cwd ??
|
|
2036
|
-
process.cwd()
|
|
2037
|
-
const baseRepo = canonicalRealpath(
|
|
2038
|
-
runRoleGit(cwd, ['rev-parse', '--show-toplevel'], dependencies).trim(),
|
|
2039
|
-
)
|
|
2040
|
-
const manifestPath = path.join(
|
|
2041
|
-
baseRepo,
|
|
2042
|
-
ROLE_WORKTREE_MANIFEST_DIR,
|
|
2043
|
-
`${requireSafeWaveId(run, 'role wave run')}.json`,
|
|
2044
|
-
)
|
|
2045
|
-
assertManagedPathParents(baseRepo, manifestPath, 'role wave manifest')
|
|
2046
|
-
const manifest = readRoleWaveManifest(manifestPath)
|
|
2047
|
-
if (
|
|
2048
|
-
manifest === undefined ||
|
|
2049
|
-
manifest.run !== run ||
|
|
2050
|
-
existingPathIdentity(manifest.baseRepo) !==
|
|
2051
|
-
existingPathIdentity(baseRepo) ||
|
|
2052
|
-
(options.requireIsolated && manifest.baseCommit === null)
|
|
2053
|
-
) {
|
|
2054
|
-
throw new RunnerUsageError(
|
|
2055
|
-
`No isolated role wave state exists for ${run}`,
|
|
2056
|
-
)
|
|
2057
|
-
}
|
|
2058
|
-
if (manifest.baseCommit !== null) {
|
|
2059
|
-
const baseCommit = requireGitOid(
|
|
2060
|
-
manifest.baseCommit,
|
|
2061
|
-
'role wave base commit',
|
|
2062
|
-
)
|
|
2063
|
-
runRoleGit(
|
|
2064
|
-
baseRepo,
|
|
2065
|
-
['cat-file', '-e', `${baseCommit}^{commit}`],
|
|
2066
|
-
dependencies,
|
|
2067
|
-
)
|
|
2068
|
-
} else if (Object.keys(manifest.worktrees).length > 0) {
|
|
2069
|
-
throw new RunnerUsageError(
|
|
2070
|
-
`Role wave ${run} has worktrees without an isolated base commit`,
|
|
2071
|
-
)
|
|
2072
|
-
}
|
|
2073
|
-
const registered = parseGitWorktreeList(
|
|
2074
|
-
runRoleGit(
|
|
2075
|
-
baseRepo,
|
|
2076
|
-
['worktree', 'list', '--porcelain', '-z'],
|
|
2077
|
-
dependencies,
|
|
2078
|
-
),
|
|
2079
|
-
)
|
|
2080
|
-
const tempRoot = ensureManagedTempRoot(
|
|
2081
|
-
dependencies.tempRoot ?? os.tmpdir(),
|
|
2082
|
-
)
|
|
2083
|
-
const repoKey = createHash('sha256')
|
|
2084
|
-
.update(baseRepo)
|
|
2085
|
-
.digest('hex')
|
|
2086
|
-
.slice(0, 16)
|
|
2087
|
-
validateRoleWaveManifestIntegrity(manifest, {
|
|
2088
|
-
baseRepo,
|
|
2089
|
-
run,
|
|
2090
|
-
tempRoot,
|
|
2091
|
-
repoKey,
|
|
2092
|
-
registered,
|
|
2093
|
-
runGit: (args) => runRoleGit(baseRepo, args, dependencies),
|
|
2094
|
-
})
|
|
2095
|
-
return { baseRepo, manifestPath, manifest, tempRoot, repoKey }
|
|
2096
|
-
}
|
|
484
|
+
export function resolveWindowsKimiBinary(env = process.env, fileExists = existsSync) {
|
|
485
|
+
const pathValue = getEnvCaseInsensitive(env, 'PATH') ?? ''
|
|
486
|
+
// npm installs three siblings on Windows: `kimi`, `kimi.cmd`, and
|
|
487
|
+
// `kimi.ps1`. The extensionless sibling is a POSIX shell script and cannot
|
|
488
|
+
// be passed to CreateProcess, while PowerShell scripts cannot be spawned
|
|
489
|
+
// directly with shell:false. Probe only executable-safe forms, with the npm
|
|
490
|
+
// command shim first so large prompts can use the stdin bootstrap.
|
|
491
|
+
const names = ['kimi.cmd', 'kimi.bat', 'kimi.exe', 'kimi.com']
|
|
2097
492
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
throw new RunnerUsageError(
|
|
2105
|
-
`Role wave manifest identity mismatch for ${context.run}`,
|
|
2106
|
-
)
|
|
2107
|
-
}
|
|
2108
|
-
const worktreeRoot = path.join(
|
|
2109
|
-
context.tempRoot,
|
|
2110
|
-
'specrails-kimi-worktrees',
|
|
2111
|
-
context.repoKey,
|
|
2112
|
-
context.run,
|
|
2113
|
-
)
|
|
2114
|
-
const executionRoot = path.join(
|
|
2115
|
-
context.tempRoot,
|
|
2116
|
-
'specrails-kimi-execution',
|
|
2117
|
-
context.repoKey,
|
|
2118
|
-
context.run,
|
|
2119
|
-
)
|
|
2120
|
-
const expectedGitExclude = path.join(
|
|
2121
|
-
context.tempRoot,
|
|
2122
|
-
'specrails-kimi-git-excludes',
|
|
2123
|
-
context.repoKey,
|
|
2124
|
-
`${context.run}.txt`,
|
|
2125
|
-
)
|
|
2126
|
-
let excludeValidated = false
|
|
2127
|
-
const validateExclude = (actual) => {
|
|
2128
|
-
if (path.resolve(actual) !== expectedGitExclude) {
|
|
2129
|
-
throw new RunnerUsageError(
|
|
2130
|
-
`Role wave manifest has an unexpected git exclude path`,
|
|
2131
|
-
)
|
|
2132
|
-
}
|
|
2133
|
-
if (!excludeValidated) {
|
|
2134
|
-
let metadata
|
|
2135
|
-
try {
|
|
2136
|
-
metadata = lstatSync(expectedGitExclude)
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
throw new RunnerUsageError(
|
|
2139
|
-
`Cannot verify role git exclude file: ${errorMessage(error)}`,
|
|
2140
|
-
)
|
|
2141
|
-
}
|
|
2142
|
-
if (
|
|
2143
|
-
!metadata.isFile() ||
|
|
2144
|
-
metadata.isSymbolicLink() ||
|
|
2145
|
-
readFileSync(expectedGitExclude, 'utf8') !== roleGitExcludeContents()
|
|
2146
|
-
) {
|
|
2147
|
-
throw new RunnerUsageError(
|
|
2148
|
-
`Invalid role git exclude file: ${expectedGitExclude}`,
|
|
2149
|
-
)
|
|
2150
|
-
}
|
|
2151
|
-
excludeValidated = true
|
|
493
|
+
for (const rawEntry of pathValue.split(';')) {
|
|
494
|
+
const entry = rawEntry.trim().replace(/^"(.*)"$/, '$1')
|
|
495
|
+
if (entry === '') continue
|
|
496
|
+
for (const name of names) {
|
|
497
|
+
const candidate = path.win32.join(entry, name)
|
|
498
|
+
if (fileExists(candidate)) return candidate
|
|
2152
499
|
}
|
|
2153
500
|
}
|
|
501
|
+
throw new RunnerUsageError(
|
|
502
|
+
'No shell-free Kimi executable (.cmd, .bat, .exe, or .com) was found on PATH',
|
|
503
|
+
)
|
|
504
|
+
}
|
|
2154
505
|
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
throw new RunnerUsageError(
|
|
2160
|
-
`Role wave manifest worktree path mismatch: ${worktree}`,
|
|
2161
|
-
)
|
|
2162
|
-
}
|
|
2163
|
-
if (
|
|
2164
|
-
!existsSync(expected) ||
|
|
2165
|
-
lstatSync(expected).isSymbolicLink() ||
|
|
2166
|
-
!lstatSync(expected).isDirectory() ||
|
|
2167
|
-
!context.registered.has(existingPathIdentity(expected))
|
|
2168
|
-
) {
|
|
2169
|
-
throw new RunnerUsageError(
|
|
2170
|
-
`Role wave worktree is not the expected registered path: ${worktree}`,
|
|
2171
|
-
)
|
|
2172
|
-
}
|
|
506
|
+
export function resolveKimiLaunch(kimiArgs, options = {}) {
|
|
507
|
+
const platform = options.platform ?? process.platform
|
|
508
|
+
if (platform !== 'win32') {
|
|
509
|
+
return { command: 'kimi', args: kimiArgs }
|
|
2173
510
|
}
|
|
2174
511
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
path.resolve(role.repoDir) !== context.baseRepo
|
|
2183
|
-
) {
|
|
2184
|
-
throw new RunnerUsageError(
|
|
2185
|
-
`Role wave manifest current workspace mismatch: ${roleKey}`,
|
|
2186
|
-
)
|
|
2187
|
-
}
|
|
2188
|
-
continue
|
|
2189
|
-
}
|
|
2190
|
-
if (!role.workspace.startsWith('worktree:')) {
|
|
2191
|
-
throw new RunnerUsageError(
|
|
2192
|
-
`Role wave manifest has invalid workspace: ${roleKey}`,
|
|
2193
|
-
)
|
|
2194
|
-
}
|
|
2195
|
-
const worktree = requireSafeWaveId(
|
|
2196
|
-
role.workspace.slice('worktree:'.length),
|
|
2197
|
-
'manifest role worktree id',
|
|
2198
|
-
)
|
|
2199
|
-
const expected = manifest.worktrees[worktree]
|
|
2200
|
-
if (
|
|
2201
|
-
expected === undefined ||
|
|
2202
|
-
path.resolve(role.executionCwd) !== expected ||
|
|
2203
|
-
path.resolve(role.repoDir) !== expected
|
|
2204
|
-
) {
|
|
2205
|
-
throw new RunnerUsageError(
|
|
2206
|
-
`Role wave manifest isolated workspace mismatch: ${roleKey}`,
|
|
2207
|
-
)
|
|
2208
|
-
}
|
|
512
|
+
const fileExists = options.fileExists ?? existsSync
|
|
513
|
+
const readFile = options.readFile ?? ((file) => readFileSync(file, 'utf8'))
|
|
514
|
+
const binary = options.binary ?? resolveWindowsKimiBinary(options.env, fileExists)
|
|
515
|
+
const extension = path.win32.extname(binary).toLowerCase()
|
|
516
|
+
if (extension !== '.cmd' && extension !== '.bat') {
|
|
517
|
+
assertWindowsCommandLineBudget(binary, kimiArgs)
|
|
518
|
+
return { command: binary, args: kimiArgs }
|
|
2209
519
|
}
|
|
2210
520
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
const refCommit = requireGitOid(
|
|
2218
|
-
context.runGit(['rev-parse', '--verify', ref]).trim(),
|
|
2219
|
-
'role wave private ref',
|
|
521
|
+
let entry
|
|
522
|
+
try {
|
|
523
|
+
entry = parseNpmCmdShimEntry(binary, readFile(binary))
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new RunnerUsageError(
|
|
526
|
+
`Cannot read the Kimi Windows shim ${binary}: ${errorMessage(error)}`,
|
|
2220
527
|
)
|
|
2221
|
-
if (refCommit !== baseCommit) {
|
|
2222
|
-
throw new RunnerUsageError(
|
|
2223
|
-
`Role wave private ref does not match its baseline: ${context.run}`,
|
|
2224
|
-
)
|
|
2225
|
-
}
|
|
2226
528
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
function runRoleGit(cwd, args, dependencies = {}, input) {
|
|
2230
|
-
const command = dependencies.gitBinary ?? 'git'
|
|
2231
|
-
const execute = dependencies.spawnSync ?? spawnSync
|
|
2232
|
-
const result = execute(
|
|
2233
|
-
command,
|
|
2234
|
-
['-C', cwd, '-c', 'core.autocrlf=false', ...args],
|
|
2235
|
-
{
|
|
2236
|
-
encoding: 'utf8',
|
|
2237
|
-
env: { ...process.env, ...(dependencies.env ?? {}) },
|
|
2238
|
-
input,
|
|
2239
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
2240
|
-
shell: false,
|
|
2241
|
-
},
|
|
2242
|
-
)
|
|
2243
|
-
if (result.error || result.status !== 0) {
|
|
2244
|
-
const detail =
|
|
2245
|
-
errorMessage(result.error ?? '').trim() ||
|
|
2246
|
-
String(result.stderr ?? '').trim() ||
|
|
2247
|
-
`exit ${String(result.status)}`
|
|
529
|
+
if (entry === null) {
|
|
2248
530
|
throw new RunnerUsageError(
|
|
2249
|
-
`
|
|
531
|
+
`Refusing to execute non-standard Kimi Windows shim through cmd.exe: ${binary}`,
|
|
2250
532
|
)
|
|
2251
533
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
return
|
|
2259
|
-
} catch {
|
|
2260
|
-
return false
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
function assertWorkspaceParentSafety(root, file) {
|
|
2265
|
-
let cursor = realpathSync(root)
|
|
2266
|
-
const relative = path.relative(path.resolve(root), path.dirname(file))
|
|
2267
|
-
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
2268
|
-
cursor = path.join(cursor, component)
|
|
2269
|
-
if (!existsSync(cursor) && !lstatExists(cursor)) break
|
|
2270
|
-
const metadata = lstatSync(cursor)
|
|
2271
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
2272
|
-
throw new RunnerUsageError(
|
|
2273
|
-
`Role merge target parent must not be a symlink: ${cursor}`,
|
|
2274
|
-
)
|
|
2275
|
-
}
|
|
534
|
+
const localNode = path.win32.join(path.win32.dirname(binary), 'node.exe')
|
|
535
|
+
const nodeBinary = fileExists(localNode) ? localNode : 'node'
|
|
536
|
+
const promptFlag = kimiArgs.indexOf('-p')
|
|
537
|
+
if (promptFlag === -1 || promptFlag + 1 >= kimiArgs.length) {
|
|
538
|
+
const args = [entry, ...kimiArgs]
|
|
539
|
+
assertWindowsCommandLineBudget(nodeBinary, args)
|
|
540
|
+
return { command: nodeBinary, args }
|
|
2276
541
|
}
|
|
2277
|
-
}
|
|
2278
542
|
|
|
2279
|
-
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
2291
|
-
throw new RunnerUsageError(
|
|
2292
|
-
`Role merge target parent must not be a symlink: ${cursor}`,
|
|
2293
|
-
)
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
543
|
+
const transportedArgs = [...kimiArgs]
|
|
544
|
+
const prompt = transportedArgs[promptFlag + 1]
|
|
545
|
+
transportedArgs[promptFlag + 1] = WINDOWS_PROMPT_STDIN_TOKEN
|
|
546
|
+
const args = [
|
|
547
|
+
'-e',
|
|
548
|
+
WINDOWS_NPM_STDIN_BOOTSTRAP,
|
|
549
|
+
entry,
|
|
550
|
+
...transportedArgs,
|
|
551
|
+
]
|
|
552
|
+
assertWindowsCommandLineBudget(nodeBinary, args)
|
|
553
|
+
return { command: nodeBinary, args, stdinText: prompt }
|
|
2296
554
|
}
|
|
2297
555
|
|
|
2298
|
-
/**
|
|
556
|
+
/** Resolve the shared backlog and pipeline runtime locations for the skill. */
|
|
2299
557
|
export function resolvePipelineEnvironment(cwd, env = process.env) {
|
|
2300
558
|
const contextPath = nonEmptyString(env.SPECRAILS_EXECUTION_CONTEXT)
|
|
2301
559
|
let context
|
|
2302
560
|
if (contextPath) {
|
|
2303
561
|
if (!path.isAbsolute(contextPath)) throw new RunnerUsageError('SPECRAILS_EXECUTION_CONTEXT must be absolute')
|
|
2304
562
|
const metadata = lstatSync(contextPath)
|
|
2305
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size >
|
|
563
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_EXECUTION_CONTEXT_BYTES) {
|
|
2306
564
|
throw new RunnerUsageError('Execution context must be a bounded regular non-symlink file')
|
|
2307
565
|
}
|
|
2308
566
|
context = JSON.parse(readFileSync(contextPath, 'utf8'))
|
|
@@ -2330,68 +588,12 @@ export function resolvePipelineEnvironment(cwd, env = process.env) {
|
|
|
2330
588
|
export async function runSkillCli(argv, dependencies = {}) {
|
|
2331
589
|
const cwd = dependencies.cwd ?? process.cwd()
|
|
2332
590
|
const parsedArgs = parseRunnerArgs(argv)
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
const env = parsedArgs.roleWaveFile !== undefined && !inputEnv.SPECRAILS_EXECUTION_CONTEXT && existsSync(admitted)
|
|
2338
|
-
? { ...inputEnv, SPECRAILS_EXECUTION_CONTEXT: admitted } : inputEnv
|
|
2339
|
-
dependencies = { ...dependencies, env: resolvePipelineEnvironment(cwd, env) }
|
|
591
|
+
dependencies = {
|
|
592
|
+
...dependencies,
|
|
593
|
+
env: resolvePipelineEnvironment(cwd, dependencies.env ?? process.env),
|
|
594
|
+
}
|
|
2340
595
|
const scriptPath = dependencies.scriptPath ?? process.argv[1]
|
|
2341
596
|
const providerRoot = resolveProviderRoot(scriptPath)
|
|
2342
|
-
const writeOutput =
|
|
2343
|
-
dependencies.writeOutput ??
|
|
2344
|
-
((line) => {
|
|
2345
|
-
process.stdout.write(line)
|
|
2346
|
-
})
|
|
2347
|
-
if (parsedArgs.roleWaveStatus !== undefined) {
|
|
2348
|
-
emitRoleFrame(
|
|
2349
|
-
writeOutput,
|
|
2350
|
-
{
|
|
2351
|
-
type: 'specrails.merge.inventory',
|
|
2352
|
-
...inspectRoleWaveStatus(parsedArgs.roleWaveStatus, {
|
|
2353
|
-
...dependencies,
|
|
2354
|
-
cwd,
|
|
2355
|
-
}),
|
|
2356
|
-
},
|
|
2357
|
-
)
|
|
2358
|
-
return 0
|
|
2359
|
-
}
|
|
2360
|
-
if (parsedArgs.roleWaveCleanup !== undefined) {
|
|
2361
|
-
emitRoleFrame(
|
|
2362
|
-
writeOutput,
|
|
2363
|
-
{
|
|
2364
|
-
type: 'specrails.role.cleanup',
|
|
2365
|
-
...cleanupRoleWave(parsedArgs.roleWaveCleanup, {
|
|
2366
|
-
...dependencies,
|
|
2367
|
-
cwd,
|
|
2368
|
-
}),
|
|
2369
|
-
},
|
|
2370
|
-
)
|
|
2371
|
-
return 0
|
|
2372
|
-
}
|
|
2373
|
-
if (parsedArgs.roleMergeFile !== undefined) {
|
|
2374
|
-
const merge = loadRoleMerge(parsedArgs, cwd)
|
|
2375
|
-
emitRoleFrame(
|
|
2376
|
-
writeOutput,
|
|
2377
|
-
{
|
|
2378
|
-
type: 'specrails.merge.applied',
|
|
2379
|
-
...applyRoleMerge(merge, {
|
|
2380
|
-
...dependencies,
|
|
2381
|
-
cwd,
|
|
2382
|
-
}),
|
|
2383
|
-
},
|
|
2384
|
-
)
|
|
2385
|
-
return 0
|
|
2386
|
-
}
|
|
2387
|
-
if (parsedArgs.roleWaveFile !== undefined) {
|
|
2388
|
-
const wave = loadRoleWave(parsedArgs, cwd)
|
|
2389
|
-
return runRoleWave(wave, {
|
|
2390
|
-
...dependencies,
|
|
2391
|
-
cwd,
|
|
2392
|
-
providerRoot,
|
|
2393
|
-
})
|
|
2394
|
-
}
|
|
2395
597
|
if (parsedArgs.plainPromptStdin) {
|
|
2396
598
|
const readStdin =
|
|
2397
599
|
dependencies.readStdin ??
|
|
@@ -2416,154 +618,13 @@ export async function runSkillCli(argv, dependencies = {}) {
|
|
|
2416
618
|
model: parsedArgs.model,
|
|
2417
619
|
})
|
|
2418
620
|
}
|
|
2419
|
-
|
|
2420
|
-
return runPreparedSkill(parsed, {
|
|
621
|
+
return runPreparedSkill(parsedArgs, {
|
|
2421
622
|
...dependencies,
|
|
2422
623
|
cwd,
|
|
2423
624
|
providerRoot,
|
|
2424
625
|
})
|
|
2425
626
|
}
|
|
2426
627
|
|
|
2427
|
-
async function runRoleWave(wave, dependencies) {
|
|
2428
|
-
const sourceEnv = dependencies.env ?? process.env
|
|
2429
|
-
const inheritedProfile = nonEmptyString(
|
|
2430
|
-
sourceEnv.SPECRAILS_PROFILE_PATH,
|
|
2431
|
-
)
|
|
2432
|
-
const contextPath = nonEmptyString(sourceEnv.SPECRAILS_EXECUTION_CONTEXT)
|
|
2433
|
-
const executionContext = contextPath ? JSON.parse(readFileSync(contextPath, 'utf8')) : undefined
|
|
2434
|
-
const repositoryCwd = nonEmptyString(sourceEnv.SPECRAILS_REPO_DIR) ??
|
|
2435
|
-
executionContext?.artifactRoot ?? dependencies.cwd
|
|
2436
|
-
if (executionContext) {
|
|
2437
|
-
if (executionContext.ownership?.worktrees === 'host' && wave.roles.some((role) => role.workspace !== 'current')) {
|
|
2438
|
-
throw new RunnerUsageError('Host-owned execution must use current repositories; sibling worktrees are not allowed')
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
const materialized = materializeRoleWaveWorkspaces(wave, {
|
|
2442
|
-
cwd: repositoryCwd,
|
|
2443
|
-
providerRoot: dependencies.providerRoot,
|
|
2444
|
-
platform: dependencies.platform,
|
|
2445
|
-
spawnSync: dependencies.spawnSync,
|
|
2446
|
-
gitBinary: dependencies.gitBinary,
|
|
2447
|
-
tempRoot: dependencies.tempRoot,
|
|
2448
|
-
})
|
|
2449
|
-
const writeOutput =
|
|
2450
|
-
dependencies.writeOutput ??
|
|
2451
|
-
((line) => {
|
|
2452
|
-
process.stdout.write(line)
|
|
2453
|
-
})
|
|
2454
|
-
const children = new Set()
|
|
2455
|
-
const aggregateChild = {
|
|
2456
|
-
kill(signal) {
|
|
2457
|
-
for (const child of children) child.kill?.(signal)
|
|
2458
|
-
},
|
|
2459
|
-
}
|
|
2460
|
-
const removeSignalForwarding = forwardTerminationSignals(
|
|
2461
|
-
aggregateChild,
|
|
2462
|
-
dependencies.signalSource ?? process,
|
|
2463
|
-
)
|
|
2464
|
-
|
|
2465
|
-
try {
|
|
2466
|
-
const results = await Promise.all(
|
|
2467
|
-
materialized.roles.map(async (role) => {
|
|
2468
|
-
emitRoleFrame(writeOutput, {
|
|
2469
|
-
type: 'specrails.role.workspace',
|
|
2470
|
-
run: materialized.run,
|
|
2471
|
-
roleKey: role.key,
|
|
2472
|
-
workspace: role.workspace,
|
|
2473
|
-
executionCwd: role.cwd,
|
|
2474
|
-
repoDir: role.repoDir,
|
|
2475
|
-
baseCommit: materialized.baseCommit,
|
|
2476
|
-
manifestPath: materialized.manifestPath,
|
|
2477
|
-
})
|
|
2478
|
-
try {
|
|
2479
|
-
const code = await runPreparedSkill(
|
|
2480
|
-
{
|
|
2481
|
-
skill: role.skill,
|
|
2482
|
-
model: role.model,
|
|
2483
|
-
rawArgs: role.rawArgs,
|
|
2484
|
-
sessionId: undefined,
|
|
2485
|
-
additionalDirs: Array.from(
|
|
2486
|
-
new Set([
|
|
2487
|
-
...wave.additionalDirs,
|
|
2488
|
-
materialized.baseRepo,
|
|
2489
|
-
...(executionContext?.repositories ?? []).map((repository) => repository.path),
|
|
2490
|
-
sourceEnv.SPECRAILS_BACKLOG_ROOT,
|
|
2491
|
-
...(contextPath ? [path.dirname(contextPath)] : []),
|
|
2492
|
-
]),
|
|
2493
|
-
),
|
|
2494
|
-
attachmentPaths: [],
|
|
2495
|
-
extraPrompt: undefined,
|
|
2496
|
-
},
|
|
2497
|
-
{
|
|
2498
|
-
...dependencies,
|
|
2499
|
-
cwd: role.cwd,
|
|
2500
|
-
providerRoot: dependencies.providerRoot,
|
|
2501
|
-
env: {
|
|
2502
|
-
...sourceEnv,
|
|
2503
|
-
SPECRAILS_REPO_DIR: role.repoDir,
|
|
2504
|
-
SPECRAILS_MERGE_TARGET: materialized.baseRepo,
|
|
2505
|
-
SPECRAILS_KIMI_WORKTREE_MANIFEST:
|
|
2506
|
-
materialized.manifestPath,
|
|
2507
|
-
...(role.profile === 'inherit'
|
|
2508
|
-
? inheritedProfile === undefined
|
|
2509
|
-
? {}
|
|
2510
|
-
: {
|
|
2511
|
-
SPECRAILS_PROFILE_PATH: path.isAbsolute(
|
|
2512
|
-
inheritedProfile,
|
|
2513
|
-
)
|
|
2514
|
-
? inheritedProfile
|
|
2515
|
-
: path.resolve(
|
|
2516
|
-
materialized.baseRepo,
|
|
2517
|
-
inheritedProfile,
|
|
2518
|
-
),
|
|
2519
|
-
}
|
|
2520
|
-
: {
|
|
2521
|
-
SPECRAILS_PROFILE_PATH: path.join(
|
|
2522
|
-
materialized.baseRepo,
|
|
2523
|
-
'.specrails',
|
|
2524
|
-
'profiles',
|
|
2525
|
-
`${role.profile}.json`,
|
|
2526
|
-
),
|
|
2527
|
-
}),
|
|
2528
|
-
GIT_CONFIG_COUNT: '2',
|
|
2529
|
-
GIT_CONFIG_KEY_0: 'core.excludesFile',
|
|
2530
|
-
GIT_CONFIG_VALUE_0: role.gitExcludeFile,
|
|
2531
|
-
GIT_CONFIG_KEY_1: 'core.autocrlf',
|
|
2532
|
-
GIT_CONFIG_VALUE_1: 'false',
|
|
2533
|
-
},
|
|
2534
|
-
captureRoleKey: role.key,
|
|
2535
|
-
writeOutput,
|
|
2536
|
-
childRegistry: children,
|
|
2537
|
-
forwardSignals: false,
|
|
2538
|
-
},
|
|
2539
|
-
)
|
|
2540
|
-
emitRoleFrame(writeOutput, {
|
|
2541
|
-
type: 'specrails.role.completed',
|
|
2542
|
-
run: materialized.run,
|
|
2543
|
-
roleKey: role.key,
|
|
2544
|
-
status: code === 0 ? 'succeeded' : 'failed',
|
|
2545
|
-
exitCode: code,
|
|
2546
|
-
})
|
|
2547
|
-
return code
|
|
2548
|
-
} catch (error) {
|
|
2549
|
-
emitRoleFrame(writeOutput, {
|
|
2550
|
-
type: 'specrails.role.completed',
|
|
2551
|
-
run: materialized.run,
|
|
2552
|
-
roleKey: role.key,
|
|
2553
|
-
status: 'failed',
|
|
2554
|
-
exitCode: 1,
|
|
2555
|
-
error: errorMessage(error),
|
|
2556
|
-
})
|
|
2557
|
-
return 1
|
|
2558
|
-
}
|
|
2559
|
-
}),
|
|
2560
|
-
)
|
|
2561
|
-
return results.find((code) => code !== 0) ?? 0
|
|
2562
|
-
} finally {
|
|
2563
|
-
removeSignalForwarding()
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
628
|
async function runPreparedSkill(parsed, dependencies) {
|
|
2568
629
|
const prepared = prepareSkillLaunch(
|
|
2569
630
|
{ ...parsed, providerRoot: dependencies.providerRoot },
|
|
@@ -2581,7 +642,6 @@ async function runPreparedSkill(parsed, dependencies) {
|
|
|
2581
642
|
|
|
2582
643
|
async function runPreparedPrompt(kimiArgs, prompt, dependencies) {
|
|
2583
644
|
assertManagedPrompt(prompt)
|
|
2584
|
-
let child
|
|
2585
645
|
let removeSignalForwarding = () => {}
|
|
2586
646
|
try {
|
|
2587
647
|
// Kimi 0.27 exposes only `-p <prompt>` for an exact non-interactive user
|
|
@@ -2596,45 +656,27 @@ async function runPreparedPrompt(kimiArgs, prompt, dependencies) {
|
|
|
2596
656
|
dependencies.env ?? process.env,
|
|
2597
657
|
dependencies.model,
|
|
2598
658
|
)
|
|
2599
|
-
child = spawnChild(launch.command, launch.args, {
|
|
659
|
+
const child = spawnChild(launch.command, launch.args, {
|
|
2600
660
|
cwd: dependencies.cwd,
|
|
2601
661
|
env,
|
|
2602
662
|
shell: false,
|
|
2603
663
|
stdio:
|
|
2604
|
-
|
|
2605
|
-
?
|
|
2606
|
-
|
|
2607
|
-
: ['pipe', 'inherit', 'inherit']
|
|
2608
|
-
: launch.stdinText === undefined
|
|
2609
|
-
? ['ignore', 'pipe', 'pipe']
|
|
2610
|
-
: ['pipe', 'pipe', 'pipe'],
|
|
664
|
+
launch.stdinText === undefined
|
|
665
|
+
? 'inherit'
|
|
666
|
+
: ['pipe', 'inherit', 'inherit'],
|
|
2611
667
|
})
|
|
2612
|
-
dependencies.childRegistry?.add(child)
|
|
2613
668
|
const completion = waitForChild(child)
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
child,
|
|
2619
|
-
dependencies.captureRoleKey,
|
|
2620
|
-
dependencies.writeOutput,
|
|
2621
|
-
)
|
|
2622
|
-
removeSignalForwarding =
|
|
2623
|
-
dependencies.forwardSignals === false
|
|
2624
|
-
? () => {}
|
|
2625
|
-
: forwardTerminationSignals(
|
|
2626
|
-
child,
|
|
2627
|
-
dependencies.signalSource ?? process,
|
|
2628
|
-
)
|
|
669
|
+
removeSignalForwarding = forwardTerminationSignals(
|
|
670
|
+
child,
|
|
671
|
+
dependencies.signalSource ?? process,
|
|
672
|
+
)
|
|
2629
673
|
if (launch.stdinText === undefined) {
|
|
2630
|
-
|
|
2631
|
-
return code
|
|
674
|
+
return await completion
|
|
2632
675
|
}
|
|
2633
676
|
try {
|
|
2634
677
|
const [code] = await Promise.all([
|
|
2635
678
|
completion,
|
|
2636
679
|
writePromptToChild(child, launch.stdinText),
|
|
2637
|
-
outputCompletion,
|
|
2638
680
|
])
|
|
2639
681
|
return code
|
|
2640
682
|
} catch (error) {
|
|
@@ -2642,7 +684,6 @@ async function runPreparedPrompt(kimiArgs, prompt, dependencies) {
|
|
|
2642
684
|
throw error
|
|
2643
685
|
}
|
|
2644
686
|
} finally {
|
|
2645
|
-
if (child !== undefined) dependencies.childRegistry?.delete(child)
|
|
2646
687
|
removeSignalForwarding()
|
|
2647
688
|
}
|
|
2648
689
|
}
|
|
@@ -2659,94 +700,6 @@ function assertManagedPrompt(prompt) {
|
|
|
2659
700
|
}
|
|
2660
701
|
}
|
|
2661
702
|
|
|
2662
|
-
function captureRoleOutput(child, roleKey, writeOutput) {
|
|
2663
|
-
const streams = [
|
|
2664
|
-
['stdout', child.stdout],
|
|
2665
|
-
['stderr', child.stderr],
|
|
2666
|
-
].filter(([, stream]) => stream?.on)
|
|
2667
|
-
if (streams.length === 0) return Promise.resolve()
|
|
2668
|
-
return Promise.all(
|
|
2669
|
-
streams.map(([streamName, stream]) =>
|
|
2670
|
-
new Promise((resolve) => {
|
|
2671
|
-
const decoder = new StringDecoder('utf8')
|
|
2672
|
-
let pending = ''
|
|
2673
|
-
let finished = false
|
|
2674
|
-
const emitLines = (final) => {
|
|
2675
|
-
const parts = pending.split('\n')
|
|
2676
|
-
pending = final ? '' : (parts.pop() ?? '')
|
|
2677
|
-
for (const line of parts) {
|
|
2678
|
-
emitCapturedRoleLine(
|
|
2679
|
-
writeOutput,
|
|
2680
|
-
roleKey,
|
|
2681
|
-
streamName,
|
|
2682
|
-
line.replace(/\r$/, ''),
|
|
2683
|
-
)
|
|
2684
|
-
}
|
|
2685
|
-
if (final && pending !== '') {
|
|
2686
|
-
emitCapturedRoleLine(
|
|
2687
|
-
writeOutput,
|
|
2688
|
-
roleKey,
|
|
2689
|
-
streamName,
|
|
2690
|
-
pending.replace(/\r$/, ''),
|
|
2691
|
-
)
|
|
2692
|
-
pending = ''
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
const finish = () => {
|
|
2696
|
-
if (finished) return
|
|
2697
|
-
finished = true
|
|
2698
|
-
pending += decoder.end()
|
|
2699
|
-
emitLines(true)
|
|
2700
|
-
resolve()
|
|
2701
|
-
}
|
|
2702
|
-
stream.on('data', (chunk) => {
|
|
2703
|
-
pending += decoder.write(
|
|
2704
|
-
Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)),
|
|
2705
|
-
)
|
|
2706
|
-
emitLines(false)
|
|
2707
|
-
})
|
|
2708
|
-
stream.once('end', finish)
|
|
2709
|
-
stream.once('close', finish)
|
|
2710
|
-
stream.once('error', (error) => {
|
|
2711
|
-
emitRoleFrame(writeOutput, {
|
|
2712
|
-
type: 'specrails.role.output-error',
|
|
2713
|
-
roleKey,
|
|
2714
|
-
stream: streamName,
|
|
2715
|
-
error: errorMessage(error),
|
|
2716
|
-
})
|
|
2717
|
-
finish()
|
|
2718
|
-
})
|
|
2719
|
-
}),
|
|
2720
|
-
),
|
|
2721
|
-
).then(() => undefined)
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
function emitCapturedRoleLine(writeOutput, roleKey, stream, line) {
|
|
2725
|
-
if (line === '') return
|
|
2726
|
-
if (stream === 'stdout') {
|
|
2727
|
-
try {
|
|
2728
|
-
emitRoleFrame(writeOutput, {
|
|
2729
|
-
type: 'specrails.role.event',
|
|
2730
|
-
roleKey,
|
|
2731
|
-
event: JSON.parse(line),
|
|
2732
|
-
})
|
|
2733
|
-
return
|
|
2734
|
-
} catch {
|
|
2735
|
-
// Preserve non-JSON output as an attributed frame.
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
emitRoleFrame(writeOutput, {
|
|
2739
|
-
type: 'specrails.role.output',
|
|
2740
|
-
roleKey,
|
|
2741
|
-
stream,
|
|
2742
|
-
data: line,
|
|
2743
|
-
})
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
function emitRoleFrame(writeOutput, value) {
|
|
2747
|
-
writeOutput(`${JSON.stringify({ role: 'meta', ...value })}\n`)
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
703
|
export function forwardTerminationSignals(child, signalSource = process) {
|
|
2751
704
|
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP']
|
|
2752
705
|
const handlers = new Map()
|
|
@@ -2867,41 +820,6 @@ function requireSafeSessionId(value) {
|
|
|
2867
820
|
return value
|
|
2868
821
|
}
|
|
2869
822
|
|
|
2870
|
-
function requireSafeWaveId(value, label) {
|
|
2871
|
-
if (typeof value !== 'string' || !SAFE_WAVE_ID.test(value)) {
|
|
2872
|
-
throw new RunnerUsageError(
|
|
2873
|
-
`Invalid ${label}: expected 1-64 lowercase letters, digits, or hyphens`,
|
|
2874
|
-
)
|
|
2875
|
-
}
|
|
2876
|
-
return value
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
function requireGitOid(value, label) {
|
|
2880
|
-
if (typeof value !== 'string' || !SAFE_GIT_OID.test(value)) {
|
|
2881
|
-
throw new RunnerUsageError(
|
|
2882
|
-
`Invalid ${label}: expected a 40- or 64-character lowercase hex commit id`,
|
|
2883
|
-
)
|
|
2884
|
-
}
|
|
2885
|
-
return value
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
function requireSafeRelativePath(value, label) {
|
|
2889
|
-
if (
|
|
2890
|
-
typeof value !== 'string' ||
|
|
2891
|
-
value.length === 0 ||
|
|
2892
|
-
Buffer.byteLength(value, 'utf8') > 4_096 ||
|
|
2893
|
-
value.includes('\0') ||
|
|
2894
|
-
value.includes('\\') ||
|
|
2895
|
-
path.posix.isAbsolute(value) ||
|
|
2896
|
-
path.posix.normalize(value) !== value ||
|
|
2897
|
-
value.split('/').some((part) => part === '' || part === '.' || part === '..') ||
|
|
2898
|
-
isManagedRolePath(value)
|
|
2899
|
-
) {
|
|
2900
|
-
throw new RunnerUsageError(`Invalid ${label}`)
|
|
2901
|
-
}
|
|
2902
|
-
return value
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
823
|
function requireNonEmpty(value, label) {
|
|
2906
824
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
2907
825
|
throw new RunnerUsageError(`Missing or empty ${label}`)
|