@kontourai/flow-agents 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +41 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const KIT_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
7
|
+
const WORKFLOW_TRIGGER_IDENTIFIER_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
8
|
+
const MAX_STEERING_VALUE_LENGTH = 240;
|
|
9
|
+
|
|
10
|
+
function readJson(file) {
|
|
11
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function containedPath(root, rel) {
|
|
15
|
+
if (typeof rel !== 'string' || path.isAbsolute(rel)) return null;
|
|
16
|
+
const resolved = path.resolve(root, rel);
|
|
17
|
+
const absRoot = path.resolve(root);
|
|
18
|
+
if (resolved === absRoot || resolved.startsWith(`${absRoot}${path.sep}`)) return resolved;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadKitManifest(kitRoot, sourceKind, warnings) {
|
|
23
|
+
const manifestPath = path.join(kitRoot, 'kit.json');
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
26
|
+
const manifest = readJson(manifestPath);
|
|
27
|
+
const kitId = typeof manifest.id === 'string' ? manifest.id : '';
|
|
28
|
+
if (!KIT_ID_RE.test(kitId)) {
|
|
29
|
+
warnings.push(`${manifestPath}: kit id must be a kebab-case id (^[a-z][a-z0-9-]*$); skipping kit`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return { kit_id: kitId, source_kind: sourceKind, manifest };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
warnings.push(`${manifestPath}: ${String(error && error.message ? error.message : error)}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readKitManifests(root) {
|
|
40
|
+
const warnings = [];
|
|
41
|
+
const kits = [];
|
|
42
|
+
const catalogPath = path.join(root, 'kits', 'catalog.json');
|
|
43
|
+
if (fs.existsSync(catalogPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const catalog = readJson(catalogPath);
|
|
46
|
+
if (Array.isArray(catalog.kits)) {
|
|
47
|
+
for (const entry of catalog.kits) {
|
|
48
|
+
const rel = entry && typeof entry === 'object' ? entry.path : undefined;
|
|
49
|
+
const kitRoot = containedPath(root, rel);
|
|
50
|
+
if (!kitRoot || !fs.existsSync(kitRoot)) continue;
|
|
51
|
+
const loaded = loadKitManifest(kitRoot, 'builtin', warnings);
|
|
52
|
+
if (loaded) kits.push(loaded);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
warnings.push(String(error && error.message ? error.message : error));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const registryPath = path.join(root, 'kits', 'local', 'installed-kits.json');
|
|
61
|
+
if (fs.existsSync(registryPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const registry = readJson(registryPath);
|
|
64
|
+
if (Array.isArray(registry.kits)) {
|
|
65
|
+
for (const entry of registry.kits) {
|
|
66
|
+
const id = entry && typeof entry === 'object' ? entry.id : undefined;
|
|
67
|
+
if (typeof id !== 'string') continue;
|
|
68
|
+
const kitRoot = path.join(root, 'kits', 'local', 'repositories', id);
|
|
69
|
+
if (!fs.existsSync(kitRoot)) continue;
|
|
70
|
+
const loaded = loadKitManifest(kitRoot, 'local', warnings);
|
|
71
|
+
if (loaded) kits.push(loaded);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
warnings.push(String(error && error.message ? error.message : error));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { kits, warnings };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isWorkflowTriggerIdentifier(value) {
|
|
82
|
+
return typeof value === 'string' && WORKFLOW_TRIGGER_IDENTIFIER_RE.test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function optionalWorkflowTriggerIdentifier(value) {
|
|
86
|
+
return value === undefined || isWorkflowTriggerIdentifier(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function optionalWorkflowTriggerIdentifierList(value) {
|
|
90
|
+
return value === undefined ||
|
|
91
|
+
(Array.isArray(value) && value.length > 0 && value.every(isWorkflowTriggerIdentifier));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function optionalConditionalSkills(value) {
|
|
95
|
+
return value === undefined ||
|
|
96
|
+
(Array.isArray(value) && value.every(item =>
|
|
97
|
+
item && typeof item === 'object' &&
|
|
98
|
+
isWorkflowTriggerIdentifier(item.when) &&
|
|
99
|
+
isWorkflowTriggerIdentifier(item.skill)
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderKitSteering(trigger) {
|
|
104
|
+
const kitId = isWorkflowTriggerIdentifier(trigger.kit_id) ? trigger.kit_id : '';
|
|
105
|
+
const category = isWorkflowTriggerIdentifier(trigger.when) ? trigger.when : 'matching';
|
|
106
|
+
const targetFlowId = isWorkflowTriggerIdentifier(trigger.target_flow_id) ? trigger.target_flow_id : '';
|
|
107
|
+
const defaultSkill = isWorkflowTriggerIdentifier(trigger.default_skill) ? trigger.default_skill : '';
|
|
108
|
+
const conditionalSkills = Array.isArray(trigger.conditional_skills) ? trigger.conditional_skills
|
|
109
|
+
.map(item => ({
|
|
110
|
+
when: isWorkflowTriggerIdentifier(item && item.when) ? item.when : '',
|
|
111
|
+
skill: isWorkflowTriggerIdentifier(item && item.skill) ? item.skill : '',
|
|
112
|
+
}))
|
|
113
|
+
.filter(item => item.when && item.skill) : [];
|
|
114
|
+
const requiredSequence = Array.isArray(trigger.required_sequence)
|
|
115
|
+
? trigger.required_sequence.filter(isWorkflowTriggerIdentifier)
|
|
116
|
+
: [];
|
|
117
|
+
const postVerifyTargets = Array.isArray(trigger.post_verify_targets)
|
|
118
|
+
? trigger.post_verify_targets.filter(isWorkflowTriggerIdentifier)
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
const lines = [
|
|
122
|
+
`KIT WORKFLOW ROUTE: this user prompt matches ${category}.`,
|
|
123
|
+
];
|
|
124
|
+
if (kitId && targetFlowId) {
|
|
125
|
+
lines.push(`Before source edits or implementation commands, use the \`${kitId}\` kit's \`${targetFlowId}\` workflow.`);
|
|
126
|
+
} else if (kitId) {
|
|
127
|
+
lines.push(`Before source edits or implementation commands, use the \`${kitId}\` kit workflow.`);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push('Before source edits or implementation commands, use the matching kit workflow.');
|
|
130
|
+
}
|
|
131
|
+
if (conditionalSkills.length || defaultSkill) {
|
|
132
|
+
const conditionalText = conditionalSkills
|
|
133
|
+
.map(item => `If ${item.when}, activate \`${item.skill}\``)
|
|
134
|
+
.join('; ');
|
|
135
|
+
if (conditionalText && defaultSkill) lines.push(`${conditionalText}; otherwise activate \`${defaultSkill}\`.`);
|
|
136
|
+
else if (conditionalText) lines.push(`${conditionalText}.`);
|
|
137
|
+
else lines.push(`Activate \`${defaultSkill}\`.`);
|
|
138
|
+
}
|
|
139
|
+
if (targetFlowId) {
|
|
140
|
+
lines.push(`Keep the session on \`${targetFlowId}\` and use \`npm run workflow:sidecar -- ensure-session --flow-id ${targetFlowId} ...\` when the repo provides the sidecar writer.`);
|
|
141
|
+
}
|
|
142
|
+
if (requiredSequence.length) {
|
|
143
|
+
lines.push(`Do not bypass ${requiredSequence.join(' -> ')} for matching work.`);
|
|
144
|
+
}
|
|
145
|
+
if (postVerifyTargets.length) {
|
|
146
|
+
lines.push(`After local verification, continue to ${postVerifyTargets.join(' and ')}; do not treat local verification as terminal delivery.`);
|
|
147
|
+
}
|
|
148
|
+
return lines.join(' ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function workflowTriggerValidationErrors(manifest, manifestPath) {
|
|
152
|
+
const errors = [];
|
|
153
|
+
const raw = manifest.workflow_triggers;
|
|
154
|
+
if (raw === undefined) return errors;
|
|
155
|
+
if (!Array.isArray(raw)) {
|
|
156
|
+
errors.push(`${manifestPath}: .workflow_triggers must be a list`);
|
|
157
|
+
return errors;
|
|
158
|
+
}
|
|
159
|
+
const seenIds = new Set();
|
|
160
|
+
raw.forEach((trigger, index) => {
|
|
161
|
+
if (!trigger || typeof trigger !== 'object') {
|
|
162
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}] must be an object`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!isWorkflowTriggerIdentifier(trigger.id)) {
|
|
166
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].id must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (seenIds.has(trigger.id)) errors.push(`${manifestPath}: workflow_triggers[${index}].id duplicates '${trigger.id}'`);
|
|
170
|
+
seenIds.add(trigger.id);
|
|
171
|
+
if (!isWorkflowTriggerIdentifier(trigger.when)) {
|
|
172
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].when must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (trigger.hint !== undefined) {
|
|
176
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].hint is retired; use structured steering fields`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (trigger.display_name !== undefined) {
|
|
180
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].display_name is not supported in workflow_triggers; use catalog/listing metadata for human-readable names`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!optionalWorkflowTriggerIdentifier(trigger.target_flow_id)) {
|
|
184
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].target_flow_id must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!optionalWorkflowTriggerIdentifier(trigger.default_skill)) {
|
|
188
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].default_skill must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!optionalConditionalSkills(trigger.conditional_skills)) {
|
|
192
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].conditional_skills must be a list of { when, skill } identifiers matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!optionalWorkflowTriggerIdentifierList(trigger.required_sequence)) {
|
|
196
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].required_sequence must be a non-empty identifier list matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!optionalWorkflowTriggerIdentifierList(trigger.post_verify_targets)) {
|
|
200
|
+
errors.push(`${manifestPath}: workflow_triggers[${index}].post_verify_targets must be a non-empty identifier list matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return errors;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function workflowTriggersFor(root, when) {
|
|
207
|
+
const result = [];
|
|
208
|
+
const inventory = readKitManifests(root);
|
|
209
|
+
for (const kit of inventory.kits) {
|
|
210
|
+
const triggers = kit.manifest.workflow_triggers;
|
|
211
|
+
if (!Array.isArray(triggers)) continue;
|
|
212
|
+
const manifestPath = path.join(root, kit.source_kind === 'local' ? 'kits/local/repositories' : 'kits', kit.kit_id, 'kit.json');
|
|
213
|
+
const parseErrors = workflowTriggerValidationErrors(kit.manifest, manifestPath);
|
|
214
|
+
if (parseErrors.length) {
|
|
215
|
+
const warning = `${kit.kit_id}: invalid workflow_triggers metadata; skipping workflow_triggers: ${parseErrors.join('; ')}`;
|
|
216
|
+
inventory.warnings.push(warning);
|
|
217
|
+
process.stderr.write(`[flow-agents] ${warning}\n`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const trigger of triggers) {
|
|
221
|
+
if (trigger.when !== when) continue;
|
|
222
|
+
const steering = renderKitSteering({ ...trigger, kit_id: kit.kit_id });
|
|
223
|
+
if (!steering) continue;
|
|
224
|
+
result.push({ kit_id: kit.kit_id, id: trigger.id, when, steering });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
MAX_STEERING_VALUE_LENGTH,
|
|
232
|
+
renderKitSteering,
|
|
233
|
+
readKitManifests,
|
|
234
|
+
workflowTriggersFor,
|
|
235
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* runnable-command.js — shared pure-CJS runnable-command-text heuristic (#412)
|
|
4
|
+
*
|
|
5
|
+
* Zero external dependencies (only string ops). Consumed by:
|
|
6
|
+
* - scripts/hooks/stop-goal-fit.js (CJS, direct require)
|
|
7
|
+
* - build/src/cli/workflow-sidecar.js (ESM compiled, via createRequire)
|
|
8
|
+
*
|
|
9
|
+
* Extracted verbatim (no logic change) from stop-goal-fit.js's former inline
|
|
10
|
+
* `isRunnableCommandText`, so the heuristic has exactly one source of truth
|
|
11
|
+
* instead of drifting between the hook (Stop-time backstop) and the sidecar
|
|
12
|
+
* (record-time rejection).
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* isRunnableCommandText(text) → boolean
|
|
16
|
+
* isAmbiguousAbsenceCommand(text) → boolean
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// WS8 (AC10b): a kind:"command" evidence ref's excerpt/command must be a literally runnable
|
|
20
|
+
// shell command, not a prose description of a manual verification step. This heuristic
|
|
21
|
+
// rejects prose so the goal-fit backstop never spawns `bash -lc "<a sentence>"` and
|
|
22
|
+
// misreports the resulting shell error as a caught false-completion. Pairs with the rule
|
|
23
|
+
// stated in context/contracts/planning-contract.md (AC11).
|
|
24
|
+
//
|
|
25
|
+
// #362 (Wave 3 coherence fix): a leading `!` negation (optionally followed by whitespace) is
|
|
26
|
+
// stripped before evaluation, mirroring isAmbiguousAbsenceCommand's own negation handling
|
|
27
|
+
// (runnable-command.js's OTHER export, above/below) -- a negated runnable command (e.g.
|
|
28
|
+
// `! grep -r foo src/`, the exact self-asserting rewrite this module's ambiguous-absence
|
|
29
|
+
// guidance recommends) is itself still a literally runnable shell command, not prose. Without
|
|
30
|
+
// this, the ambiguous-absence advisory's own suggested remediation would be rejected by this
|
|
31
|
+
// function's fatal runnability guard -- a self-defeating loop. Only a BARE leading `!` is
|
|
32
|
+
// stripped (a single strip, not recursive); the remainder is evaluated by the existing checks
|
|
33
|
+
// unchanged, so a genuinely non-command sentence that happens to start with `!` (e.g.
|
|
34
|
+
// `! a prose sentence. It fails.`) still fails the sentence-like-prose check below and is
|
|
35
|
+
// rejected, fail-closed exactly as before.
|
|
36
|
+
function isRunnableCommandText(text) {
|
|
37
|
+
const s = typeof text === 'string' ? text.trim() : '';
|
|
38
|
+
if (!s) return false;
|
|
39
|
+
const unnegated = s.replace(/^!\s*/, '');
|
|
40
|
+
if (!unnegated) return false;
|
|
41
|
+
|
|
42
|
+
const prosePunctuation = /[.!?]\s+\S|[;:]\s+[A-Za-z]/;
|
|
43
|
+
const startsLikeSentence = /^[A-Z][A-Za-z]+(?:\s+[A-Z][A-Za-z]+|\s+[a-z]{2,})/.test(unnegated);
|
|
44
|
+
if (prosePunctuation.test(unnegated) && startsLikeSentence) return false;
|
|
45
|
+
|
|
46
|
+
const assignment = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)/;
|
|
47
|
+
let rest = unnegated;
|
|
48
|
+
let sawAssignment = false;
|
|
49
|
+
// Strip leading KEY=value prefixes before evaluating the actual command token.
|
|
50
|
+
// A line containing only assignments is still a valid shell command.
|
|
51
|
+
// eslint-disable-next-line no-constant-condition
|
|
52
|
+
while (true) {
|
|
53
|
+
const m = rest.match(assignment);
|
|
54
|
+
if (!m) break;
|
|
55
|
+
sawAssignment = true;
|
|
56
|
+
rest = rest.slice(m[0].length).trimStart();
|
|
57
|
+
if (!rest) return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const first = rest.split(/\s+/)[0] || '';
|
|
61
|
+
if (!first) return sawAssignment;
|
|
62
|
+
// WS8 (AC10b, iteration 2): the first-token allowlist is broadened to the common
|
|
63
|
+
// real-world verify binaries a project's runnable evidence names (git, tsc, eslint,
|
|
64
|
+
// python/python3, docker, curl, jq, diff, grep, test) so honest command evidence is not
|
|
65
|
+
// misclassified as prose. Still fail-closed for genuinely non-command prose: a bare
|
|
66
|
+
// sentence whose first token is none of these (and carries no path prefix or shell
|
|
67
|
+
// metacharacter) is rejected rather than executed.
|
|
68
|
+
const knownBinary = /^(npm|npx|pnpm|yarn|node|bash|sh|zsh|make|just|task|cargo|go|pytest|tox|deno|bun|ruby|rake|mvn|gradle|dotnet|swift|flutter|dart|git|tsc|eslint|python|python3|docker|curl|jq|diff|grep|test|exit|cd|false|true|set|unset|export|echo|\[|return|:|source|\.)$/.test(first)
|
|
69
|
+
|| first.startsWith('./') || first.startsWith('/') || first.includes('/');
|
|
70
|
+
if (knownBinary) {
|
|
71
|
+
const remainder = rest.slice(first.length).trim();
|
|
72
|
+
if (!remainder) return true;
|
|
73
|
+
if (isCommandShapedRemainder(remainder)) return true;
|
|
74
|
+
return !isPlainEnglishRemainder(remainder);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Shell operators are only a command signal when the first token itself looks like a
|
|
78
|
+
// command word. Incidental punctuation in prose ("External CI (green)", "done: shipped")
|
|
79
|
+
// must not make the text runnable.
|
|
80
|
+
const commandLikeToken = /^[a-z_][a-z0-9._+-]*$/i.test(first) && first === first.toLowerCase();
|
|
81
|
+
const hasCommandPositionOperator = /(?:^|\s)(?:&&|\|\||[|;])(?:\s|$)|[$`]/.test(rest);
|
|
82
|
+
if (commandLikeToken && hasCommandPositionOperator && !/[;:]\s+[A-Za-z]{2,}/.test(rest)) return true;
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isCommandShapedRemainder(remainder) {
|
|
88
|
+
const tokens = remainder.split(/\s+/).filter(Boolean);
|
|
89
|
+
if (tokens.length === 0) return true;
|
|
90
|
+
if (/['"`|&;<>()${}]/.test(remainder)) return true;
|
|
91
|
+
return tokens.some((token) => {
|
|
92
|
+
return /^--?[A-Za-z0-9][A-Za-z0-9_-]*(?:=.*)?$/.test(token)
|
|
93
|
+
|| /^[A-Za-z_][A-Za-z0-9_]*=/.test(token)
|
|
94
|
+
|| token.startsWith('./')
|
|
95
|
+
|| token.includes('/')
|
|
96
|
+
|| /\.[A-Za-z0-9]{1,12}(?::\d+)?$/.test(token);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPlainEnglishRemainder(remainder) {
|
|
101
|
+
const tokens = remainder.split(/\s+/).filter(Boolean);
|
|
102
|
+
if (tokens.length < 3) return false;
|
|
103
|
+
return tokens.every((token) => /^[A-Za-z]+[,.]?$/.test(token));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// #362: a BARE (non-negated, non-self-asserting) `grep`/`diff` invocation is exit-code
|
|
107
|
+
// AMBIGUOUS for its two most common uses — a "the thing is absent" check (author intends
|
|
108
|
+
// exit 1 = PASS) and a "the thing is present" check (author intends exit 1 = FAIL) look
|
|
109
|
+
// byte-identical to any downstream consumer that only sees the exit code. This heuristic
|
|
110
|
+
// recognizes that SHAPE (narrowly: grep/diff only, per the plan's explicit two-binary
|
|
111
|
+
// carve-out — see plan body "#362 root cause") so callers can classify a bare exit-1
|
|
112
|
+
// result as ambiguous/NOT_VERIFIED instead of guessing pass or fail.
|
|
113
|
+
//
|
|
114
|
+
// A command is NOT ambiguous (i.e. already self-asserting, exit code already encodes the
|
|
115
|
+
// author's intent) when ANY of:
|
|
116
|
+
// - it is negated with a leading `!` (`! grep ...` — negation flips 0/1 into the
|
|
117
|
+
// intended fail/pass, so exit 0 unambiguously means "pattern is present/absent" per
|
|
118
|
+
// the author's own assertion)
|
|
119
|
+
// - it is part of a `||`/`&&` boolean chain (`grep ... || true`, `grep ... && echo ok`)
|
|
120
|
+
// — the chain itself already asserts what a given branch outcome means
|
|
121
|
+
// - it is piped into a count-assertion (`grep -c ... | grep -qx 0`, `grep ... | wc -l`
|
|
122
|
+
// style) — the author is asserting on a MATCH COUNT, not on grep's own raw exit code
|
|
123
|
+
// - its first real command token (after stripping `bash -lc '...'` wrapping and any
|
|
124
|
+
// leading `KEY=value` env assignments) is not exactly `grep` or `diff`
|
|
125
|
+
//
|
|
126
|
+
// FROZEN, narrowly-scoped by design (plan item 4): exactly two binaries, one ambiguous
|
|
127
|
+
// exit code each (1). Do NOT grow this into a general per-binary exit-code table — that
|
|
128
|
+
// is explicitly rejected in the plan (unreliable for pipelines/subshells/compound
|
|
129
|
+
// commands). This function classifies EXIT-CODE ambiguity only; it has no opinion on
|
|
130
|
+
// (and does not touch) the recorded command's regex dialect/flags (BRE vs ERE, -E/-P/-G,
|
|
131
|
+
// etc.) — that is a SEPARATE invariant, enforced at replay time by
|
|
132
|
+
// `resolveTrustedCommand`/`runBackstop` (scripts/hooks/stop-goal-fit.js, search
|
|
133
|
+
// "DIALECT-PRESERVATION INVARIANT"): the recorded command string is replayed verbatim on
|
|
134
|
+
// every backstop re-run path so a `grep -E '(foo|bar)'`-style ERE construct is never
|
|
135
|
+
// silently reinterpreted as BRE.
|
|
136
|
+
function isAmbiguousAbsenceCommand(text) {
|
|
137
|
+
const s = typeof text === 'string' ? text.trim() : '';
|
|
138
|
+
if (!s) return false;
|
|
139
|
+
|
|
140
|
+
// Unwrap a `bash -lc '<command>'` / `sh -c "<command>"` style wrapper so the real
|
|
141
|
+
// command is inspected, not the shell launcher. Only a single level of unwrapping is
|
|
142
|
+
// attempted — this stays a narrow heuristic, not a shell parser.
|
|
143
|
+
let inner = s;
|
|
144
|
+
const wrapped = inner.match(/^(?:bash|sh|zsh)\s+-\S*c\s+(['"])([\s\S]*)\1\s*$/);
|
|
145
|
+
if (wrapped) inner = wrapped[2].trim();
|
|
146
|
+
|
|
147
|
+
if (!inner) return false;
|
|
148
|
+
|
|
149
|
+
// Negation anywhere at the start of the (unwrapped) command already asserts intent.
|
|
150
|
+
if (/^!\s*/.test(inner)) return false;
|
|
151
|
+
|
|
152
|
+
// A `||`/`&&` boolean chain already asserts what a given exit code means for the
|
|
153
|
+
// overall command (e.g. `grep ... || true`, `grep ... && echo found`).
|
|
154
|
+
if (/\|\|/.test(inner) || /&&/.test(inner)) return false;
|
|
155
|
+
|
|
156
|
+
// Strip leading `KEY=value` environment assignments (e.g. `FOO=bar grep ...`) to find
|
|
157
|
+
// the first REAL command token.
|
|
158
|
+
let rest = inner;
|
|
159
|
+
// eslint-disable-next-line no-constant-condition
|
|
160
|
+
while (true) {
|
|
161
|
+
const m = rest.match(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+(\S[\s\S]*)$/);
|
|
162
|
+
if (!m) break;
|
|
163
|
+
rest = m[1].trim();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A pipe (any `|`, since `||` was already excluded above) into a count-assertion
|
|
167
|
+
// (`grep -c ... | grep -qx 0`, `... | wc -l`) already asserts intent on the MATCH
|
|
168
|
+
// COUNT, not on grep/diff's own raw exit code — so ANY pipe makes this "not a bare
|
|
169
|
+
// invocation" for this narrow heuristic: a bare absence-style grep/diff is by
|
|
170
|
+
// definition not piped anywhere.
|
|
171
|
+
if (rest.includes('|')) return false;
|
|
172
|
+
|
|
173
|
+
const first = rest.trim().split(/\s+/)[0] || '';
|
|
174
|
+
return first === 'grep' || first === 'diff';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { isRunnableCommandText, isAmbiguousAbsenceCommand };
|