harness-everything 0.31.3 → 0.33.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/CHANGELOG.md +21 -0
- package/README.md +3 -2
- package/ci/mechanism-2az-step-type-registry.test.js +923 -0
- package/ci/mechanism-2ba-step-type-docs.test.js +166 -0
- package/ci/openai-plugin-package.test.js +1 -1
- package/ci/release-consistency-check.js +26 -9
- package/ci/release-consistency-check.test.js +30 -0
- package/ci/router-workflow-plan.test.js +36 -0
- package/code-review/SKILL.md +41 -0
- package/code-review/references/review-playbook.md +40 -0
- package/code-review/templates/finding.template.md +33 -0
- package/docs/openai-plugin.md +1 -1
- package/docs/workflows/code-review.md +58 -0
- package/evals/code-review/eval.yaml +26 -0
- package/evals/code-review/tasks/negative-grill-me.yaml +16 -0
- package/evals/code-review/tasks/negative-security-review.yaml +16 -0
- package/evals/code-review/tasks/negative-tdd.yaml +16 -0
- package/evals/code-review/tasks/negative-verification-loop.yaml +16 -0
- package/evals/code-review/tasks/positive-pr-review.yaml +16 -0
- package/evals/code-review/tasks/positive-revised-head-rereview.yaml +16 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/router-workflow-plan.md +18 -0
- package/harness-everything/references/skill-registry.md +1 -0
- package/harness-everything/scripts/kernel-router.js +37 -5
- package/harness-everything/scripts/routing-keywords.json +7 -0
- package/hooks/scripts/codex-user-prompt.js +5 -2
- package/hooks/scripts/lib/binding-resolver.js +1 -0
- package/hooks/scripts/lib/step-types.json +37 -0
- package/hooks/scripts/lib/workflow-obligations.js +117 -14
- package/hooks/scripts/workflow-disposition.js +16 -9
- package/hooks/scripts/workflow-stop-gate.js +31 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +5 -2
- package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +1 -0
- package/plugins/harness-everything/hooks/scripts/lib/step-types.json +37 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +117 -14
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +16 -9
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +31 -8
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +41 -0
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +40 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +33 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +18 -0
- package/plugins/harness-everything/skills/harness-everything/references/skill-registry.md +1 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +37 -5
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +7 -0
- package/sbom.cdx.json +7 -7
|
@@ -118,6 +118,24 @@ Examples:
|
|
|
118
118
|
|
|
119
119
|
There is no universal `TODO → TDD → verification` pipeline.
|
|
120
120
|
|
|
121
|
+
## Step types
|
|
122
|
+
|
|
123
|
+
The allowed step types are the entries of `hooks/scripts/lib/step-types.json`. The list below is rendered from that registry and checked against it by `ci/mechanism-2ba-step-type-docs.test.js`; examples elsewhere may name a subset.
|
|
124
|
+
|
|
125
|
+
<!-- step-types:start -->
|
|
126
|
+
- `behavior-change` — required bindings: `tdd` — forbidden bindings: none
|
|
127
|
+
- `commit` — required bindings: `git-commit` — forbidden bindings: none
|
|
128
|
+
- `doc-change` — required bindings: none — forbidden bindings: none
|
|
129
|
+
- `review` — required bindings: none — forbidden bindings: `tdd`
|
|
130
|
+
- `verification` — required bindings: `verification-loop` — forbidden bindings: none
|
|
131
|
+
<!-- step-types:end -->
|
|
132
|
+
|
|
133
|
+
`doc-change` is declared-only: a plan author chooses it explicitly, and router classification, keyword routing and System One never infer or insert it. It has no default bindings, and an explicitly declared `tdd` binding stays legal. Mixed documentation and code work is written as ordered `doc-change` and `behavior-change` steps, and one shared verification step may verify both.
|
|
134
|
+
|
|
135
|
+
Old runtimes reject a persisted non-Fable `doc-change` plan visibly at the controller and Stop boundaries (`planning:steps-invalid`). The old retained router does not show that rejection: it shows no active step. Persisted bytes are not renamed or reset. Inert Fable planning metadata is not rejected by old runtimes, because step bindings are disabled there.
|
|
136
|
+
|
|
137
|
+
Registry revisions: binding defaults and required/forbidden binding policy are materialized when a plan is recorded and are not reapplied on ordinary load. Existing typed plans must still reference currently registered step types; removing a type can invalidate a plan rather than migrate or waive its stored bindings. A policy fingerprint or migration path is future work and is not implemented today.
|
|
138
|
+
|
|
121
139
|
## Binding and disclosure rule
|
|
122
140
|
|
|
123
141
|
Each ordered requirement or Fable stage may declare `requiredBindings` and `optionalBindings` by stable id and concrete repository-relative path. Only the active requirement or dependency-ready stage exposes its paths. Required bindings must resolve before that step/stage passes; unknown required bindings remain visible and unresolved. Future-step paths and keyword-matched documents are never preloaded.
|
|
@@ -18,6 +18,7 @@ Every skill in this repository is reachable from this router. If a task matches
|
|
|
18
18
|
| `grill-me` | Tier 2 | User asks to be grilled on a plan (pure Q&A). |
|
|
19
19
|
| `improve-codebase-architecture` | Tier 2 | Deep architectural analysis / refactor planning. |
|
|
20
20
|
| `eval-harness` | Tier 2 | Scoring, stress testing, benchmark comparison. |
|
|
21
|
+
| `code-review` | Tier 2 | Explicit review requests of a PR/diff/finished implementation: correctness, contract, and negative-path findings with evidence tiers, read-only by default — not threat modeling (`security-review`), plan interrogation (`grill-me`), or gate running (`verification-loop`). |
|
|
21
22
|
| `verification-loop` | Tier 2/3 gate | Before declaring done or creating a PR. |
|
|
22
23
|
| `security-review` | Tier 2/3 gate | Changes touching auth, input handling, secrets, network boundaries. |
|
|
23
24
|
| `fable-mode` | Tier 3 | v3 staged planning, named-agent delegation, model selection, failable gates, and cold review. |
|
|
@@ -33,6 +33,14 @@ function issueMemoryCapability(workflow) {
|
|
|
33
33
|
return token;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function needsStepTypeRegistry(plan) {
|
|
37
|
+
return Boolean(
|
|
38
|
+
plan && plan.strategySelection === 'selected' &&
|
|
39
|
+
['tier2', 'tier3'].includes(plan.tier) && plan.strategy &&
|
|
40
|
+
!String(plan.strategy).startsWith('fable-')
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// Host-generated background-task notifications arrive as UserPromptSubmit
|
|
37
45
|
// hook payloads but are not user input (#168). Routing them like prompts
|
|
38
46
|
// replaces the workflow contract mid-turn (e.g. deferred -> a new Tier-2).
|
|
@@ -44,11 +52,28 @@ function isHostNotificationPrompt(prompt) {
|
|
|
44
52
|
return /^<task-notification[\s>][\s\S]*<\/task-notification>\s*$/i.test(text);
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
function stepTypeIdsFor(hooksRoot) {
|
|
56
|
+
try {
|
|
57
|
+
return require(path.join(hooksRoot, 'lib/workflow-obligations')).stepTypeIds();
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
function persistWorkflow(contract, payload, prompt) {
|
|
48
64
|
const plan = contract && contract.workflowPlan;
|
|
49
65
|
if (!payload || !(payload.session_id || payload.sessionId)) return null;
|
|
50
66
|
const { runtime, hooksRoot } = loadRuntime();
|
|
51
67
|
const context = runtime.loadWorkflow(payload);
|
|
68
|
+
const hasRetainedWorkflow = Boolean(context.workflow && runtime.OPEN_STATES.has(context.workflow.state));
|
|
69
|
+
const activePlan = hasRetainedWorkflow ? context.workflow.workflowPlan : plan;
|
|
70
|
+
if (needsStepTypeRegistry(activePlan)) {
|
|
71
|
+
try { require(path.join(hooksRoot, 'lib/workflow-obligations')).loadStepTypeRegistry(); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (hasRetainedWorkflow) return { ...context, hooksRoot, retained: true, registryError: error };
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
52
77
|
const { writeCurrentSession } = require(path.join(hooksRoot, 'lib/harness-state'));
|
|
53
78
|
writeCurrentSession(context.root, context.sessionId);
|
|
54
79
|
if (isHostNotificationPrompt(prompt)) {
|
|
@@ -121,8 +146,8 @@ function activeExecutionFor(persisted, plan) {
|
|
|
121
146
|
const obligations = require(path.join(persisted.hooksRoot, 'lib/workflow-obligations'));
|
|
122
147
|
const loaded = obligations.loadObligationSet(persisted, plan);
|
|
123
148
|
return { activeStep: loaded.set ? obligations.activeStepSnapshot(loaded.set) : null };
|
|
124
|
-
} catch (
|
|
125
|
-
return
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return { error: error && error.message ? error.message : String(error) };
|
|
126
151
|
}
|
|
127
152
|
}
|
|
128
153
|
|
|
@@ -163,10 +188,15 @@ function run(raw) {
|
|
|
163
188
|
if (persisted?.retained && persisted.workflow) {
|
|
164
189
|
if (persisted.hostNotification) console.log('\n=> Host notification: routing unchanged; keeping the active execution contract.');
|
|
165
190
|
else console.log('\n=> Retaining unresolved workflow; showing its compact active contract.');
|
|
166
|
-
const activeExecution =
|
|
191
|
+
const activeExecution = persisted.registryError
|
|
192
|
+
? { error: persisted.registryError.message || String(persisted.registryError) }
|
|
193
|
+
: activeExecutionFor(persisted, plan) || {};
|
|
167
194
|
const activeStep = activeExecution.activeStep || null;
|
|
168
195
|
const activeStages = activeExecution.activeStages || [];
|
|
169
196
|
core.printRetainedWorkflow(plan, persisted.workflow, activeStep, activeStages);
|
|
197
|
+
if (activeExecution.error) {
|
|
198
|
+
console.log(' - [Workflow Reminder] Active-step context unavailable: ' + activeExecution.error);
|
|
199
|
+
}
|
|
170
200
|
if (activeStep) {
|
|
171
201
|
const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
|
|
172
202
|
console.log(' - Resolve only bindings for this active step, then pass this step with evidence.');
|
|
@@ -205,8 +235,10 @@ function run(raw) {
|
|
|
205
235
|
if (plan.strategySelection === 'selected' && ['tier2', 'tier3'].includes(plan.tier)) {
|
|
206
236
|
console.log(' - Planning contract: ' + path.join(persisted.sessionDir, 'workflow-obligations.json'));
|
|
207
237
|
console.log(' - Requirements first: decompose intent into requirement fragments, then confirm the smallest sufficient workflow.');
|
|
208
|
-
|
|
209
|
-
console.log(' - Record
|
|
238
|
+
const stepTypeIds = stepTypeIdsFor(persisted.hooksRoot);
|
|
239
|
+
console.log(' - Record ordered step types' + (stepTypeIds ? ' (' + stepTypeIds.join(', ') + ')' : '') + ' and their bindings before execution.');
|
|
240
|
+
console.log(' - Replace <step-type-id> with a registered type you choose explicitly; mixed docs+code work uses separate doc-change and behavior-change steps; keep the verification step.');
|
|
241
|
+
console.log(' - Record plan: node "' + controller + '" plan --session-id "' + persisted.sessionId + '" --requirements-json "[{\\"id\\":\\"req-1\\",\\"stepType\\":\\"<step-type-id>\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"},{\\"id\\":\\"req-verify\\",\\"stepType\\":\\"verification\\",\\"summary\\":\\"...\\",\\"acceptance\\":\\"...\\"}]" --strategy "' + plan.strategy + '" --evidence "<why this workflow fits>"');
|
|
210
242
|
}
|
|
211
243
|
console.log(' - Enter selected workflow after planning: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
|
|
212
244
|
console.log(' - Escape one declared stage: node "' + controller + '" escape --session-id "' + persisted.sessionId + '" --stage-id "<id>" --reason-code workflow-uncovered-scope --scope "<uncovered scope>" --evidence "<evidence>"');
|
|
@@ -611,6 +611,13 @@
|
|
|
611
611
|
"bindings": [
|
|
612
612
|
"zoom-out"
|
|
613
613
|
]
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"id": "review",
|
|
617
|
+
"bindings": [
|
|
618
|
+
"code-review"
|
|
619
|
+
],
|
|
620
|
+
"regex": "\\b(?:code[- ]review|pr review)\\b|\\b(?:re-)?review\\s+(?:(?:(?:this|the|my)\\s+)?(?:pr\\b|pull request\\b|diff\\b|(?:finished\\s+)?implementation\\b(?!\\s+plan\\b))|the\\s+(?:fix\\b|new\\s+head\\b))|程式碼審查|代码审查"
|
|
614
621
|
}
|
|
615
622
|
]
|
|
616
623
|
}
|
|
@@ -24,8 +24,11 @@ function finish() {
|
|
|
24
24
|
clearTimeout(timer);
|
|
25
25
|
if (!ROUTER) process.exit(0);
|
|
26
26
|
const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const stderr = String(result.stderr || '');
|
|
28
|
+
if (stderr) process.stderr.write(stderr);
|
|
29
|
+
const registryDiagnostics = stderr.split(/\r?\n/)
|
|
30
|
+
.filter(line => line.includes('cannot load step-type registry') && /(repair|restore|plugin:sync)/i.test(line));
|
|
31
|
+
const context = [String(result.stdout || '').trim(), ...registryDiagnostics].filter(Boolean).join('\n').trim();
|
|
29
32
|
if (context) {
|
|
30
33
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } }));
|
|
31
34
|
}
|
|
@@ -40,6 +40,7 @@ const PACKAGED_BINDING_PATHS = {
|
|
|
40
40
|
'git-commit': 'git-commit/SKILL.md',
|
|
41
41
|
'security-review': 'security-review/SKILL.md',
|
|
42
42
|
'review-guidance': 'harness-everything/references/triage-and-tiers.md',
|
|
43
|
+
'code-review': 'code-review/SKILL.md',
|
|
43
44
|
};
|
|
44
45
|
const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
45
46
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "behavior-change",
|
|
4
|
+
"requiredBindings": ["tdd"],
|
|
5
|
+
"optionalBindings": [],
|
|
6
|
+
"forbiddenBindings": [],
|
|
7
|
+
"evidenceHint": "Evidence should show the requested behavior changed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "commit",
|
|
11
|
+
"requiredBindings": ["git-commit"],
|
|
12
|
+
"optionalBindings": [],
|
|
13
|
+
"forbiddenBindings": [],
|
|
14
|
+
"evidenceHint": "Evidence should identify the authorized commit."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "doc-change",
|
|
18
|
+
"requiredBindings": [],
|
|
19
|
+
"optionalBindings": [],
|
|
20
|
+
"forbiddenBindings": [],
|
|
21
|
+
"evidenceHint": "Evidence should show the requested document update."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "review",
|
|
25
|
+
"requiredBindings": [],
|
|
26
|
+
"optionalBindings": [],
|
|
27
|
+
"forbiddenBindings": ["tdd"],
|
|
28
|
+
"evidenceHint": "Evidence should identify the reviewed scope and findings."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "verification",
|
|
32
|
+
"requiredBindings": ["verification-loop"],
|
|
33
|
+
"optionalBindings": [],
|
|
34
|
+
"forbiddenBindings": [],
|
|
35
|
+
"evidenceHint": "Evidence should record objective checks appropriate to the change."
|
|
36
|
+
}
|
|
37
|
+
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const { atomicWriteJson, readJson } = require('./fable-contracts');
|
|
5
6
|
const { resolveBinding, PACKAGED_BINDING_PATHS: BINDING_PATHS } = require('./binding-resolver');
|
|
@@ -8,13 +9,102 @@ const OBLIGATION_SCHEMA_VERSION = 1;
|
|
|
8
9
|
const OBLIGATION_FILE = 'workflow-obligations.json';
|
|
9
10
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
11
|
const ALLOWED_ESCAPE_REASONS = new Set(['workflow-uncovered-scope', 'host-capability-unavailable']);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
12
|
+
const STEP_TYPE_REGISTRY_FILE = path.join(__dirname, 'step-types.json');
|
|
13
|
+
|
|
14
|
+
function validateStepTypeRegistry(registry) {
|
|
15
|
+
const errors = [];
|
|
16
|
+
if (!Array.isArray(registry) || registry.length === 0) {
|
|
17
|
+
throw new Error('invalid step-type registry: expected a non-empty array');
|
|
18
|
+
}
|
|
19
|
+
const idCounts = new Map();
|
|
20
|
+
const bindingFields = ['requiredBindings', 'optionalBindings', 'forbiddenBindings'];
|
|
21
|
+
for (const entry of registry) {
|
|
22
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
23
|
+
errors.push('entry must be an object');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const id = typeof entry.id === 'string' ? entry.id : '';
|
|
27
|
+
const validId = SAFE_ID.test(id) && id === id.trim();
|
|
28
|
+
const label = validId ? `step type ${id}` : 'step type with invalid id';
|
|
29
|
+
if (!validId) errors.push(`${label} must declare a stable id`);
|
|
30
|
+
else idCounts.set(id, (idCounts.get(id) || 0) + 1);
|
|
31
|
+
if (typeof entry.evidenceHint !== 'string' || !entry.evidenceHint.trim()) {
|
|
32
|
+
errors.push(`${label} evidenceHint must be a non-empty string`);
|
|
33
|
+
}
|
|
34
|
+
const valuesByField = new Map();
|
|
35
|
+
for (const field of bindingFields) {
|
|
36
|
+
if (!Array.isArray(entry[field])) {
|
|
37
|
+
errors.push(`${label} ${field} must be an array`);
|
|
38
|
+
valuesByField.set(field, []);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const values = [];
|
|
42
|
+
for (const binding of entry[field]) {
|
|
43
|
+
if (typeof binding !== 'string' || !SAFE_ID.test(binding) || binding !== binding.trim()) {
|
|
44
|
+
errors.push(`${label} ${field} entries must be stable binding ids`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
values.push(binding);
|
|
48
|
+
}
|
|
49
|
+
const unique = [...new Set(values)].sort();
|
|
50
|
+
if (unique.length !== values.length) errors.push(`${label} ${field} contains duplicate binding ids`);
|
|
51
|
+
valuesByField.set(field, unique);
|
|
52
|
+
}
|
|
53
|
+
for (const [left, right] of [
|
|
54
|
+
['requiredBindings', 'optionalBindings'],
|
|
55
|
+
['requiredBindings', 'forbiddenBindings'],
|
|
56
|
+
['optionalBindings', 'forbiddenBindings'],
|
|
57
|
+
]) {
|
|
58
|
+
const rightValues = new Set(valuesByField.get(right) || []);
|
|
59
|
+
for (const binding of valuesByField.get(left) || []) {
|
|
60
|
+
if (rightValues.has(binding)) errors.push(`${label} has conflicting ${left} and ${right} default for ${binding}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const id of [...idCounts.keys()].sort()) {
|
|
65
|
+
if (idCounts.get(id) > 1) errors.push(`duplicate step type id ${id}`);
|
|
66
|
+
}
|
|
67
|
+
if (errors.length) throw new Error('invalid step-type registry: ' + [...new Set(errors)].sort().join('; '));
|
|
68
|
+
return registry.map(entry => ({
|
|
69
|
+
id: entry.id,
|
|
70
|
+
requiredBindings: [...entry.requiredBindings],
|
|
71
|
+
optionalBindings: [...entry.optionalBindings],
|
|
72
|
+
forbiddenBindings: [...entry.forbiddenBindings],
|
|
73
|
+
evidenceHint: entry.evidenceHint.trim(),
|
|
74
|
+
})).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const STEP_TYPE_REGISTRY_UNAVAILABLE = 'STEP_TYPE_REGISTRY_UNAVAILABLE';
|
|
78
|
+
|
|
79
|
+
function readStepTypeRegistry(registryFile = STEP_TYPE_REGISTRY_FILE) {
|
|
80
|
+
const resolvedRegistryFile = path.resolve(registryFile);
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(fs.readFileSync(resolvedRegistryFile, 'utf8'));
|
|
83
|
+
return validateStepTypeRegistry(parsed);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const unavailable = new Error(
|
|
86
|
+
`cannot load step-type registry ${resolvedRegistryFile}: ${error.message}. ` +
|
|
87
|
+
`Repair action: reinstall or update the Harness plugin so this file is restored; in a Harness source checkout, restore it and run npm run plugin:sync.`,
|
|
88
|
+
);
|
|
89
|
+
unavailable.code = STEP_TYPE_REGISTRY_UNAVAILABLE;
|
|
90
|
+
throw unavailable;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Loaded on first typed use only, so registry-independent planning and execution never read it.
|
|
95
|
+
let stepTypeRegistry = null;
|
|
96
|
+
function loadStepTypeRegistry() {
|
|
97
|
+
if (!stepTypeRegistry) stepTypeRegistry = readStepTypeRegistry();
|
|
98
|
+
return stepTypeRegistry;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stepTypeIds() {
|
|
102
|
+
return loadStepTypeRegistry().map(entry => entry.id).sort();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stepTypeIndex() {
|
|
106
|
+
return new Map(loadStepTypeRegistry().map(entry => [entry.id, entry]));
|
|
107
|
+
}
|
|
18
108
|
|
|
19
109
|
function obligationPath(sessionDir) {
|
|
20
110
|
return path.join(sessionDir, OBLIGATION_FILE);
|
|
@@ -125,6 +215,14 @@ function initializePlanningContract(context, plan) {
|
|
|
125
215
|
return set;
|
|
126
216
|
}
|
|
127
217
|
|
|
218
|
+
function typedStepType(rawStepType) {
|
|
219
|
+
const registered = stepTypeIndex();
|
|
220
|
+
if (!registered.has(rawStepType)) {
|
|
221
|
+
throw new Error(`unsupported requirement stepType: ${rawStepType}; registered step types: ${stepTypeIds().join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
return registered.get(rawStepType);
|
|
224
|
+
}
|
|
225
|
+
|
|
128
226
|
function parseRequirements(requirementsJson, workspaceRoot) {
|
|
129
227
|
let parsed;
|
|
130
228
|
try { parsed = JSON.parse(String(requirementsJson || '')); }
|
|
@@ -145,7 +243,7 @@ function parseRequirements(requirementsJson, workspaceRoot) {
|
|
|
145
243
|
? [...new Set(item.suggestedSkills.filter(value => typeof value === 'string' && value.trim()).map(value => value.trim()))]
|
|
146
244
|
: [];
|
|
147
245
|
const rawStepType = item.stepType === undefined ? '' : String(item.stepType).trim();
|
|
148
|
-
|
|
246
|
+
const stepType = rawStepType ? typedStepType(rawStepType) : null;
|
|
149
247
|
for (const field of ['requiredBindings', 'optionalBindings']) {
|
|
150
248
|
if (Object.prototype.hasOwnProperty.call(item, field) && !Array.isArray(item[field])) {
|
|
151
249
|
throw new Error(`${id}.${field} must be an array`);
|
|
@@ -155,17 +253,18 @@ function parseRequirements(requirementsJson, workspaceRoot) {
|
|
|
155
253
|
if (!rawStepType) return requirement;
|
|
156
254
|
|
|
157
255
|
const requiredBindings = normalizeBindingDeclarations([
|
|
158
|
-
...(
|
|
256
|
+
...(stepType.requiredBindings || []),
|
|
159
257
|
...(item.requiredBindings || []),
|
|
160
258
|
]);
|
|
161
259
|
const requiredIds = new Set(requiredBindings.map(binding => binding.id));
|
|
162
260
|
const optionalBindings = normalizeBindingDeclarations([
|
|
261
|
+
...(stepType.optionalBindings || []),
|
|
163
262
|
...(item.optionalBindings || []),
|
|
164
263
|
...suggestedSkills,
|
|
165
264
|
]).filter(binding => !requiredIds.has(binding.id));
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
265
|
+
const forbiddenBinding = [...requiredBindings, ...optionalBindings]
|
|
266
|
+
.find(binding => stepType.forbiddenBindings.includes(binding.id));
|
|
267
|
+
if (forbiddenBinding) throw new Error(`${rawStepType} steps cannot bind ${forbiddenBinding.id}`);
|
|
169
268
|
requirement.stepType = rawStepType;
|
|
170
269
|
requirement.status = 'pending';
|
|
171
270
|
requirement.evidence = null;
|
|
@@ -268,7 +367,7 @@ function recordPlanning(context, requirementsJson, selectedPlan, requestedStrate
|
|
|
268
367
|
}
|
|
269
368
|
const isFable = String(selectedPlan.strategy || '').startsWith('fable-');
|
|
270
369
|
if (!isFable && !hasTypedSteps) {
|
|
271
|
-
throw new Error(
|
|
370
|
+
throw new Error(`selected single-agent workflows require ordered stepType values: ${stepTypeIds().join(', ')}`);
|
|
272
371
|
}
|
|
273
372
|
if (!isFable && !requirements.some(item => item.stepType === 'verification')) {
|
|
274
373
|
throw new Error('step-bound workflows require an explicit verification step');
|
|
@@ -360,7 +459,8 @@ function validateObligationSet(context, set, plan = context.workflow?.pendingPla
|
|
|
360
459
|
if (!['pending', 'pass', 'escaped', 'blocked'].includes(obligation.status)) return 'obligation-status-invalid';
|
|
361
460
|
}
|
|
362
461
|
if (set.stepBindingsEnabled === true) {
|
|
363
|
-
|
|
462
|
+
const registered = stepTypeIndex();
|
|
463
|
+
if (set.requirements.some(item => !item || !registered.has(item.stepType) ||
|
|
364
464
|
!['pending', 'pass', 'blocked'].includes(item.status) || !Array.isArray(item.requiredBindings) ||
|
|
365
465
|
!Array.isArray(item.optionalBindings))) return 'steps-invalid';
|
|
366
466
|
for (const step of set.requirements) {
|
|
@@ -585,6 +685,9 @@ function unresolvedObligations(context) {
|
|
|
585
685
|
module.exports = {
|
|
586
686
|
OBLIGATION_FILE,
|
|
587
687
|
ALLOWED_ESCAPE_REASONS,
|
|
688
|
+
loadStepTypeRegistry,
|
|
689
|
+
stepTypeIds,
|
|
690
|
+
validateStepTypeRegistry,
|
|
588
691
|
obligationPath,
|
|
589
692
|
needsPlanningContract,
|
|
590
693
|
needsGenericExecutionObligations,
|
|
@@ -7,15 +7,20 @@ const { loadWorkflow, saveWorkflow, matchingRun, OPEN_STATES, WORKFLOW_CONTROLLE
|
|
|
7
7
|
const { getWorkspaceRoot, readCurrentSession } = require('./lib/harness-state');
|
|
8
8
|
const { atomicWriteJson, readJson } = require('./lib/fable-contracts');
|
|
9
9
|
const { resolveBinding } = require('./lib/binding-resolver');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
|
|
11
|
+
let obligationApi;
|
|
12
|
+
function workflowObligations() {
|
|
13
|
+
if (!obligationApi) obligationApi = require('./lib/workflow-obligations');
|
|
14
|
+
return obligationApi;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const planningUnresolved = (...args) => workflowObligations().planningUnresolved(...args);
|
|
18
|
+
const recordPlanning = (...args) => workflowObligations().recordPlanning(...args);
|
|
19
|
+
const materializeExecutionObligations = (...args) => workflowObligations().materializeExecutionObligations(...args);
|
|
20
|
+
const updateObligation = (...args) => workflowObligations().updateObligation(...args);
|
|
21
|
+
const updateBinding = (...args) => workflowObligations().updateBinding(...args);
|
|
22
|
+
const updateStep = (...args) => workflowObligations().updateStep(...args);
|
|
23
|
+
const activeStepSnapshot = (...args) => workflowObligations().activeStepSnapshot(...args);
|
|
19
24
|
|
|
20
25
|
const ALLOWED_ESCAPE_REASONS = new Set(['workflow-uncovered-scope', 'host-capability-unavailable']);
|
|
21
26
|
|
|
@@ -149,6 +154,8 @@ function activatePlan(workflow, plan) {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
function main() {
|
|
157
|
+
// The step-type registry loads lazily through the wrappers above, only for commands that
|
|
158
|
+
// consume typed obligations; the top-level handler turns a load failure into one semantic line.
|
|
152
159
|
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
153
160
|
process.stdout.write(usage() + '\n');
|
|
154
161
|
return;
|
|
@@ -3,35 +3,58 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { readJson } = require('./lib/fable-contracts');
|
|
5
5
|
const { loadWorkflow, saveWorkflow, matchingRun, unresolvedStages, readHookInput } = require('./lib/workflow-runtime');
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
const REGISTRY_UNAVAILABLE = 'step-type-registry-unavailable';
|
|
8
|
+
const STEP_TYPE_REGISTRY_UNAVAILABLE = 'STEP_TYPE_REGISTRY_UNAVAILABLE';
|
|
7
9
|
|
|
8
10
|
function warn(message) { console.error('[Workflow Reminder] ' + message); }
|
|
9
11
|
|
|
10
12
|
function decide(payload) {
|
|
11
13
|
if (!payload) return;
|
|
12
14
|
try {
|
|
15
|
+
const { planningUnresolved, unresolvedObligations } = require('./lib/workflow-obligations');
|
|
13
16
|
const context = loadWorkflow(payload);
|
|
14
17
|
const { workflow, sessionDir } = context;
|
|
15
18
|
if (!workflow || workflow.state === 'deferred') return;
|
|
16
19
|
const handoff = readJson(path.join(sessionDir, 'handoff-state.json'));
|
|
17
|
-
|
|
20
|
+
const unresolved = [];
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
let requiredFault = false;
|
|
23
|
+
|
|
24
|
+
// Only a validation that actually reads the registry can fail here. It records the
|
|
25
|
+
// semantic diagnostic and the unresolved token itself; other checks keep running.
|
|
26
|
+
const required = check => {
|
|
27
|
+
try {
|
|
28
|
+
unresolved.push(...check());
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code !== STEP_TYPE_REGISTRY_UNAVAILABLE) throw error;
|
|
31
|
+
diagnostics.push(error.message);
|
|
32
|
+
unresolved.push(REGISTRY_UNAVAILABLE);
|
|
33
|
+
requiredFault = true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
18
36
|
|
|
37
|
+
required(() => planningUnresolved(context));
|
|
19
38
|
if (String(workflow.strategy || '').startsWith('fable-')) {
|
|
20
39
|
const match = matchingRun(context);
|
|
21
40
|
unresolved.push(...(match ? unresolvedStages(match, workflow, handoff?.lastEditAt || 0) : ['correlated-run-missing']));
|
|
22
41
|
} else {
|
|
23
|
-
|
|
42
|
+
required(() => unresolvedObligations(context));
|
|
24
43
|
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
25
44
|
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved.push('verification-after-edit-missing');
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
const pending = [...new Set(unresolved)];
|
|
29
48
|
const wasBlocked = workflow.state === 'blocked';
|
|
30
49
|
if (wasBlocked) warn('Workflow is marked BLOCKED; report or revisit the recorded blocker, but Harness will not trap the session.');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
[...new Set(diagnostics)].forEach(message => warn(message));
|
|
51
|
+
if (pending.length) {
|
|
52
|
+
// A failed required validation leaves the workflow file as recorded; it is never marked satisfied.
|
|
53
|
+
if (!requiredFault) {
|
|
54
|
+
workflow.unresolved = pending;
|
|
55
|
+
saveWorkflow(context);
|
|
56
|
+
}
|
|
57
|
+
warn('Unresolved workflow evidence: ' + pending.join(', ') + '. Resolve/verify before claiming completion.');
|
|
35
58
|
return;
|
|
36
59
|
}
|
|
37
60
|
if (wasBlocked) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
|
|
5
5
|
"main": "harness-everything/scripts/bootstrap.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test:consistency": "node ci/consistency-check.js && node ci/doc-capability-consistency.test.js && node ci/repository-contract.test.js && node ci/mechanism-2ao-plugin-sync.test.js && node ci/prompt-contract-regressions.test.js",
|
|
21
21
|
"test:docs:capabilities": "node ci/doc-capability-consistency.test.js",
|
|
22
22
|
"test:references": "node ci/reference-check.js",
|
|
23
|
-
"test:release": "node ci/release-consistency-check.js && node ci/mechanism-2x-release-automation.test.js && node ci/mechanism-2x-release-commit-message.test.js && node ci/mechanism-2ap-sbom.test.js",
|
|
23
|
+
"test:release": "node ci/release-consistency-check.js && node ci/release-consistency-check.test.js && node ci/mechanism-2x-release-automation.test.js && node ci/mechanism-2x-release-commit-message.test.js && node ci/mechanism-2ap-sbom.test.js",
|
|
24
24
|
"test:release:semantics": "node ci/semantic-release-analyzer.test.mjs",
|
|
25
25
|
"test:install": "node scripts/verify-install.js",
|
|
26
26
|
"system-one:install": "node harness-everything/scripts/system-one/install.js",
|
|
@@ -24,8 +24,11 @@ function finish() {
|
|
|
24
24
|
clearTimeout(timer);
|
|
25
25
|
if (!ROUTER) process.exit(0);
|
|
26
26
|
const result = spawnSync(process.execPath, [ROUTER], { input, encoding: 'utf8', env: process.env, timeout: 25000 });
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const stderr = String(result.stderr || '');
|
|
28
|
+
if (stderr) process.stderr.write(stderr);
|
|
29
|
+
const registryDiagnostics = stderr.split(/\r?\n/)
|
|
30
|
+
.filter(line => line.includes('cannot load step-type registry') && /(repair|restore|plugin:sync)/i.test(line));
|
|
31
|
+
const context = [String(result.stdout || '').trim(), ...registryDiagnostics].filter(Boolean).join('\n').trim();
|
|
29
32
|
if (context) {
|
|
30
33
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } }));
|
|
31
34
|
}
|
|
@@ -40,6 +40,7 @@ const PACKAGED_BINDING_PATHS = {
|
|
|
40
40
|
'git-commit': 'git-commit/SKILL.md',
|
|
41
41
|
'security-review': 'security-review/SKILL.md',
|
|
42
42
|
'review-guidance': 'harness-everything/references/triage-and-tiers.md',
|
|
43
|
+
'code-review': 'code-review/SKILL.md',
|
|
43
44
|
};
|
|
44
45
|
const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
45
46
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "behavior-change",
|
|
4
|
+
"requiredBindings": ["tdd"],
|
|
5
|
+
"optionalBindings": [],
|
|
6
|
+
"forbiddenBindings": [],
|
|
7
|
+
"evidenceHint": "Evidence should show the requested behavior changed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "commit",
|
|
11
|
+
"requiredBindings": ["git-commit"],
|
|
12
|
+
"optionalBindings": [],
|
|
13
|
+
"forbiddenBindings": [],
|
|
14
|
+
"evidenceHint": "Evidence should identify the authorized commit."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "doc-change",
|
|
18
|
+
"requiredBindings": [],
|
|
19
|
+
"optionalBindings": [],
|
|
20
|
+
"forbiddenBindings": [],
|
|
21
|
+
"evidenceHint": "Evidence should show the requested document update."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "review",
|
|
25
|
+
"requiredBindings": [],
|
|
26
|
+
"optionalBindings": [],
|
|
27
|
+
"forbiddenBindings": ["tdd"],
|
|
28
|
+
"evidenceHint": "Evidence should identify the reviewed scope and findings."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "verification",
|
|
32
|
+
"requiredBindings": ["verification-loop"],
|
|
33
|
+
"optionalBindings": [],
|
|
34
|
+
"forbiddenBindings": [],
|
|
35
|
+
"evidenceHint": "Evidence should record objective checks appropriate to the change."
|
|
36
|
+
}
|
|
37
|
+
]
|