@michaelxwb/code-flow 0.6.4 → 0.8.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 +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -40
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +67 -35
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -40
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +67 -35
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -40
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +67 -35
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -40
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +68 -41
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1714
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -380
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +37 -3
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +252 -26
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -299
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
9
|
+
function projectRoot(cwd) {
|
|
10
|
+
let current = path.resolve(cwd);
|
|
11
|
+
while (true) {
|
|
12
|
+
if (fs.existsSync(path.join(current, '.code-flow/config.yml'))) return current;
|
|
13
|
+
const parent = path.dirname(current);
|
|
14
|
+
if (parent === current) throw new Error('Run code-flow inside an initialized project.');
|
|
15
|
+
current = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commands(root) {
|
|
20
|
+
const registry = JSON.parse(fs.readFileSync(path.join(root, '.code-flow/runtime-commands.json'), 'utf8'));
|
|
21
|
+
if (registry.schema_version !== 1) throw new Error('Unsupported runtime command contract.');
|
|
22
|
+
return registry.commands;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectCommandFamily(name) {
|
|
26
|
+
if (!name || name.startsWith('-')) return false;
|
|
27
|
+
let current = path.resolve(process.cwd());
|
|
28
|
+
while (true) {
|
|
29
|
+
if (fs.existsSync(path.join(current, '.code-flow/runtime-commands.json'))) {
|
|
30
|
+
try {
|
|
31
|
+
return Object.keys(commands(current)).some(command => command.split(' ')[0] === name);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
process.stderr.write(`code-flow contract: ${error.message}\n`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const parent = path.dirname(current);
|
|
38
|
+
if (parent === current) return false;
|
|
39
|
+
current = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function versionRepair(root) {
|
|
44
|
+
const file = path.join(root, '.code-flow/.version');
|
|
45
|
+
if (fs.existsSync(file)) {
|
|
46
|
+
const version = fs.readFileSync(file, 'utf8').trim();
|
|
47
|
+
if (/^\d+\.\d+\.\d+$/.test(version)) {
|
|
48
|
+
const installed = version.split('.').map(Number);
|
|
49
|
+
const current = pkg.version.split('.').map(Number);
|
|
50
|
+
const index = installed.findIndex((value, position) => value !== current[position]);
|
|
51
|
+
if (index >= 0 && installed[index] > current[index]) {
|
|
52
|
+
return `CLI ${pkg.version} is older than project runtime ${version}. Upgrade the CLI: npm install --global ${pkg.name}@${version}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 'code-flow migrate --runtime --dry-run';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function digest(file) {
|
|
60
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function artifactDigest(relative, source) {
|
|
64
|
+
if (relative !== '.opencode/plugins/code-flow/package.json') return digest(source);
|
|
65
|
+
const data = JSON.parse(fs.readFileSync(source, 'utf8'));
|
|
66
|
+
data.version = pkg.version;
|
|
67
|
+
return crypto.createHash('sha256').update(JSON.stringify(data, null, 2) + '\n').digest('hex');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function filesUnder(directory, prefix) {
|
|
71
|
+
if (!fs.existsSync(directory)) return [];
|
|
72
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
73
|
+
if (entry.name === '__pycache__' || entry.name === '.DS_Store') return [];
|
|
74
|
+
const relative = path.posix.join(prefix, entry.name);
|
|
75
|
+
return entry.isDirectory() ? filesUnder(path.join(directory, entry.name), relative) : [relative];
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function artifactSources(platforms) {
|
|
80
|
+
const base = path.join(__dirname, 'core/code-flow');
|
|
81
|
+
const sources = { '.code-flow/runtime-commands.json': path.join(base, 'runtime-commands.json') };
|
|
82
|
+
for (const relative of filesUnder(path.join(base, 'scripts'), 'scripts')) {
|
|
83
|
+
sources[`.code-flow/${relative}`] = path.join(base, relative);
|
|
84
|
+
}
|
|
85
|
+
for (const platform of platforms) {
|
|
86
|
+
const adapter = path.join(__dirname, 'adapters', platform);
|
|
87
|
+
const area = platform === 'codex' ? 'skills' : 'commands';
|
|
88
|
+
const destination = platform === 'codex' ? '.agents' : `.${platform}`;
|
|
89
|
+
for (const relative of filesUnder(path.join(adapter, area), area)) {
|
|
90
|
+
sources[`${destination}/${relative}`] = path.join(adapter, relative);
|
|
91
|
+
}
|
|
92
|
+
if (platform === 'opencode') {
|
|
93
|
+
for (const relative of filesUnder(path.join(adapter, 'plugins'), 'plugins')) {
|
|
94
|
+
sources[`.opencode/${relative}`] = path.join(adapter, relative);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sources;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hookContracts(platforms) {
|
|
102
|
+
const contracts = {};
|
|
103
|
+
for (const platform of platforms.filter(value => value !== 'opencode')) {
|
|
104
|
+
const name = platform === 'codex' ? 'hooks.json' : 'settings.local.json';
|
|
105
|
+
const source = path.join(__dirname, 'adapters', platform, name);
|
|
106
|
+
contracts[`.${platform}/${name}`] = JSON.parse(fs.readFileSync(source, 'utf8')).hooks;
|
|
107
|
+
}
|
|
108
|
+
return contracts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeInstall(root, platforms) {
|
|
112
|
+
const files = Object.fromEntries(Object.entries(artifactSources(platforms))
|
|
113
|
+
.map(([relative, source]) => [relative, artifactDigest(relative, source)]));
|
|
114
|
+
const manifest = { schema_version: 1, version: pkg.version, platforms,
|
|
115
|
+
files, hook_contracts: hookContracts(platforms) };
|
|
116
|
+
const target = path.join(root, '.code-flow/.runtime-install.json');
|
|
117
|
+
fs.writeFileSync(`${target}.tmp`, JSON.stringify(manifest, null, 2) + '\n');
|
|
118
|
+
fs.renameSync(`${target}.tmp`, target);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function verifyHooks(root, contracts) {
|
|
122
|
+
for (const [relative, expected] of Object.entries(contracts)) {
|
|
123
|
+
const actual = JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8')).hooks;
|
|
124
|
+
for (const [event, groups] of Object.entries(expected)) {
|
|
125
|
+
for (const group of groups) {
|
|
126
|
+
const candidates = (actual[event] || []).filter(item => (item.matcher || '') === (group.matcher || ''));
|
|
127
|
+
for (const hook of group.hooks) {
|
|
128
|
+
if (!candidates.some(item => item.hooks.some(value => JSON.stringify(value) === JSON.stringify(hook)))) {
|
|
129
|
+
throw new Error(`Managed hook has drifted: ${relative}:${event}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function verify(root, duringMigration = false) {
|
|
138
|
+
if (!duringMigration && fs.existsSync(path.join(root, '.code-flow/.runtime-migration.lock'))) {
|
|
139
|
+
throw new Error('Runtime migration is in progress or needs recovery.');
|
|
140
|
+
}
|
|
141
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, '.code-flow/.runtime-install.json'), 'utf8'));
|
|
142
|
+
const version = fs.readFileSync(path.join(root, '.code-flow/.version'), 'utf8').trim();
|
|
143
|
+
if (manifest.schema_version !== 1 || manifest.version !== version || version !== pkg.version) {
|
|
144
|
+
throw new Error(`Runtime ${version} and CLI ${pkg.version} versions differ.`);
|
|
145
|
+
}
|
|
146
|
+
if (!manifest.files || Object.keys(manifest.files).length === 0 || !manifest.hook_contracts) {
|
|
147
|
+
throw new Error('Runtime installation manifest is incomplete.');
|
|
148
|
+
}
|
|
149
|
+
const expected = artifactSources(manifest.platforms);
|
|
150
|
+
if (Object.keys(expected).length !== Object.keys(manifest.files).length) {
|
|
151
|
+
throw new Error('Installed artifact set differs from the CLI contract.');
|
|
152
|
+
}
|
|
153
|
+
for (const [relative, source] of Object.entries(expected)) {
|
|
154
|
+
if (manifest.files[relative] !== artifactDigest(relative, source)) {
|
|
155
|
+
throw new Error(`Installed contract differs from the CLI package: ${relative}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [relative, hash] of Object.entries(manifest.files)) {
|
|
159
|
+
const target = path.resolve(root, relative);
|
|
160
|
+
if (!target.startsWith(root + path.sep) || digest(target) !== hash) {
|
|
161
|
+
throw new Error(`Managed runtime artifact has drifted: ${relative}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
verifyHooks(root, manifest.hook_contracts);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function execute(root, contract, values, scripts = path.join(root, '.code-flow/scripts')) {
|
|
168
|
+
try {
|
|
169
|
+
if (values.length === 1 || (values.length === 2 && ['--help', '-h'].includes(values[1]))) {
|
|
170
|
+
const family = Object.keys(contract).filter(command => command.startsWith(values[0] + ' '));
|
|
171
|
+
if (family.length) {
|
|
172
|
+
process.stdout.write(`Usage:\n${family.map(command => ` code-flow ${command} [options]`).join('\n')}\nUse <command> --help for its actual parameters.\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const key = Object.keys(contract).sort((a, b) => b.length - a.length)
|
|
177
|
+
.find(command => command.split(' ').every((part, index) => values[index] === part));
|
|
178
|
+
if (!key) throw new Error(`Unknown runtime command. Available: ${Object.keys(contract).join(', ')}`);
|
|
179
|
+
const entry = contract[key];
|
|
180
|
+
if (!/^cf_[a-z_]+$/.test(entry.module)) throw new Error('Invalid runtime module in command contract.');
|
|
181
|
+
const script = path.join(scripts, `${entry.module}.py`);
|
|
182
|
+
const args = [...entry.prefix, ...values.slice(key.split(' ').length)];
|
|
183
|
+
const publicName = entry.prefix.length ? key.split(' ').slice(0, -entry.prefix.length).join(' ') : key;
|
|
184
|
+
const result = spawnSync('python3', [script, ...args], {
|
|
185
|
+
cwd: root, stdio: 'inherit', env: { ...process.env,
|
|
186
|
+
CF_RUNTIME_COMMAND: `code-flow ${publicName}`, CF_RUNTIME_ACTION: entry.prefix.join(' '),
|
|
187
|
+
CF_CLI_VERSION: pkg.version },
|
|
188
|
+
});
|
|
189
|
+
if (result.error) throw result.error;
|
|
190
|
+
return result.status === null ? 2 : result.status;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
process.stderr.write(`code-flow runtime: ${error.message}\n`);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function run(values) {
|
|
198
|
+
let root;
|
|
199
|
+
try {
|
|
200
|
+
root = projectRoot(process.cwd());
|
|
201
|
+
} catch (error) {
|
|
202
|
+
process.stderr.write(`code-flow runtime: ${error.message}\n`);
|
|
203
|
+
return 3;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
if (values[0] === 'spec' && values[1] === 'doctor') {
|
|
207
|
+
const base = path.join(__dirname, 'core/code-flow');
|
|
208
|
+
const contract = JSON.parse(fs.readFileSync(path.join(base, 'runtime-commands.json'), 'utf8')).commands;
|
|
209
|
+
return execute(root, { 'spec doctor': contract['spec doctor'] }, values, path.join(base, 'scripts'));
|
|
210
|
+
}
|
|
211
|
+
verify(root);
|
|
212
|
+
return execute(root, commands(root), values);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
process.stderr.write(`code-flow installation: ${error.message}\nRepair: ${versionRepair(root)}\n`);
|
|
215
|
+
return 3;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { artifactSources, commands, digest, hookContracts, projectCommandFamily, projectRoot, run, verify, writeInstall };
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""Append execution history without erasing the user's acceptance contract."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import re
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Mapping, Sequence
|
|
9
|
-
from uuid import uuid4
|
|
10
|
-
|
|
11
|
-
STATES = frozenset(("planned", "pending", "tbd", "verified", "unverified", "failed", "incomplete", "not_configured", "e2e_deferred", "manual_pending"))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def scenario_line(line: str, scenario_id: str) -> bool:
|
|
15
|
-
return bool(re.match(rf"^\s*(?:\|\s*|[-*]\s+){re.escape(scenario_id)}(?=\s*[:|])", line))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def line_status(line: str, scenario_id: str) -> str:
|
|
19
|
-
if not scenario_line(line, scenario_id):
|
|
20
|
-
return ""
|
|
21
|
-
if line.lstrip().startswith("|"):
|
|
22
|
-
cells = [cell.strip().lower() for cell in line.strip().strip("|").split("|")]
|
|
23
|
-
return next((cell for cell in reversed(cells) if cell in STATES), "")
|
|
24
|
-
match = re.match(r"^\s*[-*]\s+[^:]+:\s*([a-z0-9_]+)\b", line, re.I)
|
|
25
|
-
return match.group(1).lower() if match and match.group(1).lower() in STATES else ""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _status_line(line: str, scenario_id: str, status: str) -> str:
|
|
29
|
-
if not scenario_line(line, scenario_id):
|
|
30
|
-
return line
|
|
31
|
-
if line.lstrip().startswith("|"):
|
|
32
|
-
cells = line.split("|")
|
|
33
|
-
for index in range(len(cells) - 1, 0, -1):
|
|
34
|
-
if cells[index].strip().lower() in STATES:
|
|
35
|
-
cells[index] = f" {status} "
|
|
36
|
-
return "|".join(cells)
|
|
37
|
-
return line.rstrip().rstrip("|") + f"| {status} |"
|
|
38
|
-
prefix = r"(^\s*[-*]\s+[^:]+:\s*)"
|
|
39
|
-
if line_status(line, scenario_id):
|
|
40
|
-
return re.sub(prefix + r"(?:" + "|".join(sorted(STATES)) + r")\b", rf"\g<1>{status}", line, count=1, flags=re.I)
|
|
41
|
-
return re.sub(prefix, rf"\g<1>{status} — ", line, count=1)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _patch_section(section: str, scenario_id: str, status: str, record: str) -> str:
|
|
45
|
-
contract = re.search(r"(?ms)^### Acceptance Contract[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
|
|
46
|
-
if contract:
|
|
47
|
-
body = "\n".join(_status_line(line, scenario_id, status) for line in contract.group(1).splitlines()) + "\n"
|
|
48
|
-
section = section[:contract.start(1)] + body + section[contract.end(1):]
|
|
49
|
-
evidence = re.search(r"(?ms)^### Acceptance Evidence[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
|
|
50
|
-
if evidence:
|
|
51
|
-
body = evidence.group(1)
|
|
52
|
-
if record not in body.splitlines():
|
|
53
|
-
body = body.rstrip() + "\n" + record + "\n\n"
|
|
54
|
-
section = section[:evidence.start(1)] + body + section[evidence.end(1):]
|
|
55
|
-
else:
|
|
56
|
-
section = section.rstrip() + "\n\n### Acceptance Evidence\n" + record + "\n"
|
|
57
|
-
return section
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def sync_task_evidence(task: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "", status: str = "verified") -> None:
|
|
61
|
-
text = task.read_text(encoding="utf-8")
|
|
62
|
-
task_pattern = re.escape(owner) if owner else r"TASK-\d+"
|
|
63
|
-
match = re.search(rf"(?ms)^##\s+{task_pattern}:.*?(?=^## |\Z)", text)
|
|
64
|
-
if match is None:
|
|
65
|
-
raise ValueError(f"acceptance owner missing: {owner}")
|
|
66
|
-
record = f"- {scenario_id}: {status} — {evidence} (confirmed_by: {confirmed_by})"
|
|
67
|
-
section = _patch_section(match.group(0), scenario_id, status, record)
|
|
68
|
-
text = text[:match.start()] + section + text[match.end():]
|
|
69
|
-
coverage = re.search(r"(?ms)^## Acceptance Coverage[^\n]*\n(.*?)(?=^## |\Z)", text)
|
|
70
|
-
if coverage:
|
|
71
|
-
body = "\n".join(_status_line(line, scenario_id, status) for line in coverage.group(1).splitlines()) + "\n"
|
|
72
|
-
text = text[:coverage.start(1)] + body + text[coverage.end(1):]
|
|
73
|
-
from cf_spec_context import _atomic_text
|
|
74
|
-
_atomic_text(task, text)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def persist_results(path: Path, data: dict[str, object], results: Sequence[Mapping[str, object]]) -> None:
|
|
78
|
-
rows = {row["id"]: row for row in data["scenarios"]}
|
|
79
|
-
run_id, executed_at = uuid4().hex, datetime.now(timezone.utc).isoformat()
|
|
80
|
-
updates = []
|
|
81
|
-
for result in results:
|
|
82
|
-
state = str(result["status"])
|
|
83
|
-
if state in ("not_included", "manual_pending"):
|
|
84
|
-
continue
|
|
85
|
-
row = rows[result["id"]]
|
|
86
|
-
row["revision"] = int(row.get("revision", 0)) + 1
|
|
87
|
-
status = "verified" if state == "passed" else state
|
|
88
|
-
row["status"] = status
|
|
89
|
-
record = {**result, "revision": row["revision"], "run_id": run_id, "executed_at": executed_at}
|
|
90
|
-
row["evidence"] = record
|
|
91
|
-
row.setdefault("runs", []).append(record)
|
|
92
|
-
updates.append((row, record, status))
|
|
93
|
-
from cf_spec_context import _atomic_text
|
|
94
|
-
_atomic_text(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
|
95
|
-
task_name = data.get("task_file")
|
|
96
|
-
if isinstance(task_name, str) and task_name:
|
|
97
|
-
task = Path(task_name) if Path(task_name).is_absolute() else path.parent / task_name
|
|
98
|
-
for row, record, status in updates:
|
|
99
|
-
detail = f"automated command {record['status']}; run_id={run_id}"
|
|
100
|
-
sync_task_evidence(task, str(row["id"]), "runner", detail, str(row.get("owner", "")), status)
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""Validated acceptance records shared by execution and lifecycle gates."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import math
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Mapping
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def verified_evidence(row: Mapping[str, object]) -> bool:
|
|
11
|
-
evidence = row.get("evidence")
|
|
12
|
-
if row.get("status") != "verified" or not isinstance(evidence, dict):
|
|
13
|
-
return False
|
|
14
|
-
if row.get("kind") != "manual":
|
|
15
|
-
return evidence.get("status") == "passed" and evidence.get("exit_code") == 0
|
|
16
|
-
author, detail = evidence.get("confirmed_by"), evidence.get("evidence")
|
|
17
|
-
agents = {"agent", "assistant", "codex", "claude", "opencode", "costrict"}
|
|
18
|
-
return (isinstance(author, str) and bool(author.strip())
|
|
19
|
-
and author.strip().lower().split(":", 1)[0] not in agents
|
|
20
|
-
and isinstance(detail, str) and bool(detail.strip()))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def validate_scenarios(value: object) -> list[dict[str, object]]:
|
|
24
|
-
if not isinstance(value, list):
|
|
25
|
-
raise ValueError("invalid manifest scenarios: expected list")
|
|
26
|
-
seen: set[str] = set()
|
|
27
|
-
for item in value:
|
|
28
|
-
if not isinstance(item, dict):
|
|
29
|
-
raise ValueError("invalid scenario: expected object")
|
|
30
|
-
scenario_id = item.get("id")
|
|
31
|
-
if not isinstance(scenario_id, str) or not scenario_id.strip():
|
|
32
|
-
raise ValueError("scenario id missing")
|
|
33
|
-
if scenario_id in seen:
|
|
34
|
-
raise ValueError(f"duplicate scenario id: {scenario_id}")
|
|
35
|
-
seen.add(scenario_id)
|
|
36
|
-
for field in ("kind", "owner", "cwd"):
|
|
37
|
-
if field in item and not isinstance(item[field], str):
|
|
38
|
-
raise ValueError(f"invalid scenario {scenario_id}:{field}")
|
|
39
|
-
command = item.get("command")
|
|
40
|
-
if command is not None and (not isinstance(command, list) or not all(isinstance(x, str) for x in command)):
|
|
41
|
-
raise ValueError(f"invalid scenario {scenario_id}:command")
|
|
42
|
-
timeout = item.get("timeout", 60)
|
|
43
|
-
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout <= 0:
|
|
44
|
-
raise ValueError(f"invalid scenario {scenario_id}:timeout")
|
|
45
|
-
deps = item.get("depends_on", [])
|
|
46
|
-
if not isinstance(deps, list) or not all(isinstance(x, str) for x in deps):
|
|
47
|
-
raise ValueError(f"invalid scenario {scenario_id}:depends_on")
|
|
48
|
-
if "runs" in item and not isinstance(item["runs"], list):
|
|
49
|
-
raise ValueError(f"invalid scenario {scenario_id}:runs")
|
|
50
|
-
revision = item.get("revision", 0)
|
|
51
|
-
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 0:
|
|
52
|
-
raise ValueError(f"invalid scenario {scenario_id}:revision")
|
|
53
|
-
for item in value:
|
|
54
|
-
unknown = set(item.get("depends_on", [])) - seen
|
|
55
|
-
if unknown:
|
|
56
|
-
raise ValueError(f"acceptance scenario dependency missing: {', '.join(sorted(unknown))}")
|
|
57
|
-
return value
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def load_manifest(path: Path) -> dict[str, object]:
|
|
61
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
-
if not isinstance(data, dict):
|
|
63
|
-
raise ValueError("invalid acceptance manifest: expected object")
|
|
64
|
-
validate_scenarios(data.get("scenarios"))
|
|
65
|
-
if "task_file" in data and not isinstance(data["task_file"], str):
|
|
66
|
-
raise ValueError("invalid acceptance manifest: task_file")
|
|
67
|
-
return data
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def validate_execution_baseline(path: Path, data: Mapping[str, object]) -> None:
|
|
71
|
-
"""Legacy standalone manifests remain usable; locked plans must match."""
|
|
72
|
-
task_name = data.get("task_file")
|
|
73
|
-
if not isinstance(task_name, str) or not task_name:
|
|
74
|
-
return
|
|
75
|
-
task = Path(task_name)
|
|
76
|
-
task = task if task.is_absolute() else path.parent / task
|
|
77
|
-
if not task.is_file():
|
|
78
|
-
raise ValueError(f"acceptance task file missing: {task}")
|
|
79
|
-
if "## Acceptance Coverage" in task.read_text(encoding="utf-8"):
|
|
80
|
-
from cf_acceptance_manifest import validate_manifest
|
|
81
|
-
valid, reason = validate_manifest(str(task), str(path))
|
|
82
|
-
if not valid:
|
|
83
|
-
raise ValueError(reason)
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Shared execution substrate: deadline-aware argv subprocess runner.
|
|
3
|
-
|
|
4
|
-
All check executors (acceptance scenarios, Spec verifiers, session
|
|
5
|
-
validators) funnel through here so one entry deadline constrains the whole
|
|
6
|
-
chain, commands never pass through a shell, and unrun work is reported as
|
|
7
|
-
`incomplete` instead of silently skipped or falsely passed.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import shlex
|
|
13
|
-
import json
|
|
14
|
-
import subprocess
|
|
15
|
-
import time
|
|
16
|
-
from contextlib import contextmanager
|
|
17
|
-
from contextvars import ContextVar
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import Iterator, Mapping, Optional, Sequence
|
|
20
|
-
|
|
21
|
-
_EXECUTIONS: ContextVar[Optional[dict[str, dict[str, object]]]] = ContextVar("cf_executions", default=None)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@contextmanager
|
|
25
|
-
def execution_session() -> Iterator[None]:
|
|
26
|
-
"""Share results only inside one validation operation, never across turns."""
|
|
27
|
-
if _EXECUTIONS.get() is not None:
|
|
28
|
-
yield
|
|
29
|
-
return
|
|
30
|
-
token = _EXECUTIONS.set({})
|
|
31
|
-
try:
|
|
32
|
-
yield
|
|
33
|
-
finally:
|
|
34
|
-
_EXECUTIONS.reset(token)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def invalidate_executions() -> None:
|
|
38
|
-
cache = _EXECUTIONS.get()
|
|
39
|
-
if cache is not None:
|
|
40
|
-
cache.clear()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def execution_key(argv: Sequence[str], cwd: str, timeout: float) -> str:
|
|
44
|
-
"""Identity of a command under the current process environment."""
|
|
45
|
-
return json.dumps([list(argv), str(Path(cwd).resolve()), float(timeout)], ensure_ascii=False)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def remaining_seconds(deadline: Optional[float]) -> Optional[float]:
|
|
49
|
-
"""Seconds left until the absolute monotonic deadline (None = unbounded)."""
|
|
50
|
-
if deadline is None:
|
|
51
|
-
return None
|
|
52
|
-
return deadline - time.monotonic()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def build_argv(template: str, files: Sequence[str]) -> list[str]:
|
|
56
|
-
"""Expand a validator command template into argv without a shell.
|
|
57
|
-
|
|
58
|
-
`{files}` must be a standalone token and expands to one argv entry per
|
|
59
|
-
file (spaces/metachars safe). Embedded usage is a config error — refusing
|
|
60
|
-
is safer than reintroducing shell splitting.
|
|
61
|
-
"""
|
|
62
|
-
tokens = shlex.split(template or "")
|
|
63
|
-
if not tokens:
|
|
64
|
-
return []
|
|
65
|
-
argv: list[str] = []
|
|
66
|
-
for token in tokens:
|
|
67
|
-
if token == "{files}":
|
|
68
|
-
argv.extend(files)
|
|
69
|
-
elif "{files}" in token:
|
|
70
|
-
raise ValueError(f"validator command must keep {{files}} as a standalone token: {template!r}")
|
|
71
|
-
else:
|
|
72
|
-
argv.append(token)
|
|
73
|
-
return argv
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def run_command(
|
|
77
|
-
argv: Sequence[str],
|
|
78
|
-
cwd: str,
|
|
79
|
-
timeout: float,
|
|
80
|
-
deadline: Optional[float] = None,
|
|
81
|
-
) -> dict[str, object]:
|
|
82
|
-
"""Run argv synchronously under timeout+deadline. Never uses a shell."""
|
|
83
|
-
key = execution_key(argv, cwd, timeout)
|
|
84
|
-
cache = _EXECUTIONS.get()
|
|
85
|
-
if cache is not None and key in cache:
|
|
86
|
-
return dict(cache[key])
|
|
87
|
-
result = _execute(argv, cwd, timeout, deadline)
|
|
88
|
-
if cache is not None and result["status"] == "ok":
|
|
89
|
-
cache[key] = result
|
|
90
|
-
return result
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _execute(argv: Sequence[str], cwd: str, timeout: float, deadline: Optional[float]) -> dict[str, object]:
|
|
94
|
-
remaining = remaining_seconds(deadline)
|
|
95
|
-
if remaining is not None and remaining <= 0:
|
|
96
|
-
return {"status": "deadline_exceeded", "returncode": None, "stdout": "", "stderr": ""}
|
|
97
|
-
effective = min(timeout, remaining) if remaining is not None else timeout
|
|
98
|
-
try:
|
|
99
|
-
proc = subprocess.run(
|
|
100
|
-
list(argv), cwd=cwd, capture_output=True, text=True, timeout=max(effective, 0.01)
|
|
101
|
-
)
|
|
102
|
-
except subprocess.TimeoutExpired as exc:
|
|
103
|
-
out = exc.stdout
|
|
104
|
-
return {
|
|
105
|
-
"status": "timeout",
|
|
106
|
-
"returncode": None,
|
|
107
|
-
"stdout": (out.decode() if isinstance(out, bytes) else (out or ""))[-4000:],
|
|
108
|
-
"stderr": "",
|
|
109
|
-
}
|
|
110
|
-
except OSError as exc:
|
|
111
|
-
return {"status": "spawn_error", "returncode": None, "stdout": "", "stderr": str(exc)}
|
|
112
|
-
return {"status": "ok", "returncode": proc.returncode, "stdout": proc.stdout or "", "stderr": proc.stderr or ""}
|