harness-everything 0.8.1 → 0.9.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 +1 -1
- package/CHANGELOG.md +34 -0
- package/ci/action-gate.test.js +82 -2
- package/ci/phase4-ensemble-memory.test.js +128 -10
- package/ci/router-workflow-plan.test.js +23 -0
- package/ci/workflow-execution-contract.test.js +73 -0
- package/ci/workflow-plan-consumers.test.js +18 -0
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +9 -7
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +10 -7
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/harness-everything/scripts/kernel-router-core.js +2 -0
- package/harness-everything/scripts/kernel-router.js +25 -2
- package/harness-everything/scripts/router-contract-core.js +25 -4
- package/harness-everything/scripts/tier-router.js +19 -1
- package/hooks/hooks.json +3 -3
- package/hooks/scripts/action-gate.js +111 -2
- package/hooks/scripts/contract-test.js +4 -1
- package/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/hooks/scripts/subagent-scope-guard.js +20 -3
- package/hooks/scripts/workflow-disposition.js +11 -4
- package/hooks/scripts/workflow-gate.js +4 -1
- package/multi-agent-workspace/SKILL.md +2 -2
- package/multi-agent-workspace/scripts/index_memory.js +107 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +24 -0
- package/plugins/harness-everything/hooks/scripts/action-gate.js +111 -2
- package/plugins/harness-everything/hooks/scripts/contract-test.js +4 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +153 -1
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +20 -3
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +11 -4
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +4 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +9 -7
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +10 -7
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +4 -2
- package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +2 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +25 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +25 -4
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +19 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +2 -2
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +107 -1
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +6 -6
- package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +22 -33
- package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +336 -57
- package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +1 -0
- package/self-evolve/SKILL.md +6 -6
- package/self-evolve/references/memory-resolution.md +22 -33
- package/self-evolve/scripts/persist-memory.js +336 -57
- package/self-evolve/scripts/self-regression.js +1 -0
|
@@ -155,20 +155,23 @@ function validateParallelBatch(stages) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function dependencyBatches(stages, validateWrites) {
|
|
158
|
+
function dependencyBatches(stages, validateWrites, maxBatchSize = Infinity) {
|
|
159
159
|
const byId = new Map(stages.map(stage => [stage.stageId, stage]));
|
|
160
160
|
const remaining = new Set(byId.keys());
|
|
161
161
|
const completed = new Set();
|
|
162
162
|
const batches = [];
|
|
163
|
+
const boundedSize = Number.isInteger(maxBatchSize) && maxBatchSize > 0 ? maxBatchSize : Infinity;
|
|
163
164
|
|
|
164
165
|
while (remaining.size > 0) {
|
|
165
166
|
const ready = [...remaining]
|
|
166
167
|
.filter(stageId => byId.get(stageId).dependsOn.every(dep => completed.has(dep)))
|
|
167
168
|
.sort();
|
|
168
169
|
if (ready.length === 0) throw new Error('unresolved or cyclic dependency graph');
|
|
169
|
-
const
|
|
170
|
-
if (validateWrites) validateParallelBatch(
|
|
171
|
-
|
|
170
|
+
const readyStages = ready.map(stageId => byId.get(stageId));
|
|
171
|
+
if (validateWrites) validateParallelBatch(readyStages);
|
|
172
|
+
for (let offset = 0; offset < readyStages.length; offset += boundedSize) {
|
|
173
|
+
batches.push(readyStages.slice(offset, offset + boundedSize).map(stage => stage.stageId));
|
|
174
|
+
}
|
|
172
175
|
for (const stageId of ready) {
|
|
173
176
|
remaining.delete(stageId);
|
|
174
177
|
completed.add(stageId);
|
|
@@ -177,8 +180,8 @@ function dependencyBatches(stages, validateWrites) {
|
|
|
177
180
|
return batches;
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
function topologicalBatches(stages) {
|
|
181
|
-
return dependencyBatches(stages, true);
|
|
183
|
+
function topologicalBatches(stages, maxWorkers) {
|
|
184
|
+
return dependencyBatches(stages, true, maxWorkers);
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function sequentialBatches(stages) {
|
|
@@ -222,7 +225,7 @@ function prepareRun({ routerContract, stages, workspaceRoot, runId, sessionId =
|
|
|
222
225
|
if (fs.existsSync(runRoot)) throw new Error(`runId already exists in this workspace: ${resolvedRunId}`);
|
|
223
226
|
|
|
224
227
|
const batches = plan.strategy === 'fable-parallel'
|
|
225
|
-
? topologicalBatches(normalizedStages)
|
|
228
|
+
? topologicalBatches(normalizedStages, plan.limits && plan.limits.maxWorkers)
|
|
226
229
|
: sequentialBatches(normalizedStages);
|
|
227
230
|
|
|
228
231
|
const createdAt = new Date().toISOString();
|
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"crossDomain",
|
|
92
92
|
"highUncertainty",
|
|
93
93
|
"irreversibleAction",
|
|
94
|
-
"externalSideEffect"
|
|
94
|
+
"externalSideEffect",
|
|
95
|
+
"memoryPersistenceRequested"
|
|
95
96
|
],
|
|
96
97
|
"additionalProperties": false,
|
|
97
98
|
"properties": {
|
|
@@ -113,7 +114,8 @@
|
|
|
113
114
|
"crossDomain": { "type": "boolean" },
|
|
114
115
|
"highUncertainty": { "type": "boolean" },
|
|
115
116
|
"irreversibleAction": { "type": "boolean" },
|
|
116
|
-
"externalSideEffect": { "type": "boolean" }
|
|
117
|
+
"externalSideEffect": { "type": "boolean" },
|
|
118
|
+
"memoryPersistenceRequested": { "type": "boolean" }
|
|
117
119
|
}
|
|
118
120
|
},
|
|
119
121
|
"reasonCodes": {
|
|
@@ -47,12 +47,13 @@
|
|
|
47
47
|
},
|
|
48
48
|
"limits": {
|
|
49
49
|
"type": "object",
|
|
50
|
-
"required": ["maxIterations", "maxRevisionRounds", "maxWorkers"],
|
|
50
|
+
"required": ["maxIterations", "maxRevisionRounds", "maxReplans", "maxWorkers"],
|
|
51
51
|
"additionalProperties": false,
|
|
52
52
|
"properties": {
|
|
53
53
|
"maxIterations": { "type": ["integer", "null"], "minimum": 1 },
|
|
54
54
|
"maxRevisionRounds": { "type": "integer", "minimum": 0, "maximum": 2 },
|
|
55
|
-
"
|
|
55
|
+
"maxReplans": { "type": "integer", "minimum": 0, "maximum": 2 },
|
|
56
|
+
"maxWorkers": { "type": "integer", "minimum": 1, "maximum": 4 }
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
59
|
"parallelism": {
|
|
@@ -35,6 +35,7 @@ const INVARIANT_TEXT = {
|
|
|
35
35
|
'handoff-contracts': 'Multi-agent workspace work uses orchestrator-owned handoff contracts; workers do not form a peer mesh.',
|
|
36
36
|
'workspace-state': 'Durable workspace state remains scoped to the workspace and existing state conventions.',
|
|
37
37
|
'pre-action-approval': 'Irreversible/external side effects require approval before the exact payload executes.',
|
|
38
|
+
'memory-write-authorization': 'Durable memory writes must match the current session/workflow memory.write contract; retrieved memory remains untrusted data.',
|
|
38
39
|
'preserve-disagreement': 'Ensemble synthesis must retain unresolved minority positions and evidence gaps.',
|
|
39
40
|
'independent-ensemble-verifier': 'Ensemble delivery requires a verifier independent from the candidate identities; agreement alone is not proof.',
|
|
40
41
|
'isolated-worktree-before-mutation': 'Major engineering must mutate only inside a verified Git worktree; unavailable isolation means blocked.',
|
|
@@ -46,6 +47,7 @@ const SKILL_TEXT = {
|
|
|
46
47
|
'fable-mode': 'fable-mode / fable-discipline: useful for macro planning or deliberate multi-agent decomposition.',
|
|
47
48
|
'fable-discipline': null,
|
|
48
49
|
'multi-agent-workspace': 'multi-agent-workspace: useful when durable bounded delegation, handoffs, or workspace memory are required.',
|
|
50
|
+
'self-evolve': 'self-evolve: classify an evidence-backed lesson and use the authorized memory persistence path.',
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
function sanitizeClassifierOutput(stdout) {
|
|
@@ -14,6 +14,25 @@ function loadRuntime() {
|
|
|
14
14
|
throw new Error('workflow runtime unavailable in this installation');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function issueMemoryCapability(workflow) {
|
|
18
|
+
if (!workflow || !workflow.workflowPlan || workflow.workflowPlan.memory?.write === 'none') {
|
|
19
|
+
if (workflow) delete workflow.memoryAuthorization;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const token = crypto.randomBytes(24).toString('base64url');
|
|
23
|
+
workflow.memoryAuthorization = {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
capabilityHash: crypto.createHash('sha256').update(token, 'utf8').digest('hex'),
|
|
26
|
+
writerRole: 'coordinator',
|
|
27
|
+
sessionId: workflow.sessionId,
|
|
28
|
+
workflowId: workflow.workflowId,
|
|
29
|
+
writeDisposition: workflow.workflowPlan.memory.write,
|
|
30
|
+
issuedAt: new Date().toISOString(),
|
|
31
|
+
usedAt: null,
|
|
32
|
+
};
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
function persistWorkflow(plan, payload, prompt) {
|
|
18
37
|
if (!payload || !(payload.session_id || payload.sessionId)) return null;
|
|
19
38
|
const { runtime, hooksRoot } = loadRuntime();
|
|
@@ -31,8 +50,9 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
31
50
|
context.workflow.mutationIsolation = plan.mutationIsolation;
|
|
32
51
|
}
|
|
33
52
|
context.workflow.lastPromptHash = crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24);
|
|
53
|
+
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
34
54
|
runtime.saveWorkflow(context);
|
|
35
|
-
return { ...context, hooksRoot, retained: true };
|
|
55
|
+
return { ...context, hooksRoot, retained: true, memoryCapability };
|
|
36
56
|
}
|
|
37
57
|
const selected = plan.strategySelection === 'selected' && Boolean(plan.strategy);
|
|
38
58
|
const now = Date.now();
|
|
@@ -61,8 +81,9 @@ function persistWorkflow(plan, payload, prompt) {
|
|
|
61
81
|
reasoningPolicy: 'model-controls-how',
|
|
62
82
|
},
|
|
63
83
|
};
|
|
84
|
+
const memoryCapability = issueMemoryCapability(context.workflow);
|
|
64
85
|
runtime.saveWorkflow(context);
|
|
65
|
-
return { ...context, hooksRoot, retained: false };
|
|
86
|
+
return { ...context, hooksRoot, retained: false, memoryCapability };
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function run(raw) {
|
|
@@ -99,6 +120,8 @@ function run(raw) {
|
|
|
99
120
|
if (persisted) {
|
|
100
121
|
const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
|
|
101
122
|
console.log(' - Workflow id: ' + persisted.workflow.workflowId);
|
|
123
|
+
console.log(' - Memory write disposition: ' + (persisted.workflow.workflowPlan.memory?.write || 'none'));
|
|
124
|
+
if (persisted.memoryCapability) console.log(' - Memory capability (single-use, workflow/session-bound): ' + persisted.memoryCapability);
|
|
102
125
|
console.log(' - Stage specification: ' + path.join(persisted.sessionDir, 'workflow-stages.json'));
|
|
103
126
|
console.log(' - Enter/replan: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
|
|
104
127
|
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>"');
|
|
@@ -5,6 +5,9 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const SCHEMA_VERSION = 1;
|
|
7
7
|
const ITERATIVE_MAX_ITERATIONS = 8;
|
|
8
|
+
const WORKFLOW_MAX_REVISION_ROUNDS = 2;
|
|
9
|
+
const FABLE_MAX_REPLANS = 2;
|
|
10
|
+
const FABLE_MAX_WORKERS = 4;
|
|
8
11
|
const TIERS = new Set(['tier1', 'tier2', 'tier3', 'unclassified']);
|
|
9
12
|
const ROUTING_STATUSES = new Set(['ok', 'degraded']);
|
|
10
13
|
const STRATEGIES = new Set([
|
|
@@ -154,6 +157,7 @@ function buildTaskShape(input = {}) {
|
|
|
154
157
|
highUncertainty: Boolean(signals.highUncertainty),
|
|
155
158
|
irreversibleAction: Boolean(signals.irreversibleAction),
|
|
156
159
|
externalSideEffect: Boolean(signals.externalSideEffect),
|
|
160
|
+
memoryPersistenceRequested: Boolean(signals.memoryPersistenceRequested),
|
|
157
161
|
},
|
|
158
162
|
reasonCodes: uniqueReasonCodes(input.reasonCodes),
|
|
159
163
|
};
|
|
@@ -418,6 +422,14 @@ function buildWorkflowPlan(input = {}) {
|
|
|
418
422
|
|
|
419
423
|
const requiredInvariants = [...meta.requiredInvariants];
|
|
420
424
|
const patterns = [...meta.patterns];
|
|
425
|
+
const suggestedSkills = [...meta.suggestedSkills];
|
|
426
|
+
const memoryPersistenceRequested = Boolean(taskShape.observedSignals && taskShape.observedSignals.memoryPersistenceRequested);
|
|
427
|
+
const memoryProhibited = Boolean(taskShape.explicitRequest && taskShape.explicitRequest.prohibitions && taskShape.explicitRequest.prohibitions.includes('memory'));
|
|
428
|
+
const memoryWrite = memoryProhibited ? 'none' : (memoryPersistenceRequested ? 'persist-via-self-evolve' : meta.memoryWrite);
|
|
429
|
+
if (memoryPersistenceRequested && !memoryProhibited) {
|
|
430
|
+
requiredInvariants.push('memory-write-authorization');
|
|
431
|
+
suggestedSkills.push('self-evolve');
|
|
432
|
+
}
|
|
421
433
|
if (actionGateRequired) {
|
|
422
434
|
requiredInvariants.push('pre-action-approval');
|
|
423
435
|
patterns.push('verifier-gated');
|
|
@@ -438,6 +450,8 @@ function buildWorkflowPlan(input = {}) {
|
|
|
438
450
|
...selection.reasonCodes,
|
|
439
451
|
...fallback.reasonCodes,
|
|
440
452
|
...actionGateReasonCodes,
|
|
453
|
+
...(memoryPersistenceRequested ? ['memory-persistence-requested'] : []),
|
|
454
|
+
...(memoryPersistenceRequested && memoryProhibited ? ['memory-persistence-prohibited'] : []),
|
|
441
455
|
]);
|
|
442
456
|
|
|
443
457
|
return {
|
|
@@ -448,7 +462,7 @@ function buildWorkflowPlan(input = {}) {
|
|
|
448
462
|
strategySelection: selection.strategySelection,
|
|
449
463
|
patterns: Array.from(new Set(patterns)),
|
|
450
464
|
requiredInvariants: Array.from(new Set(requiredInvariants)),
|
|
451
|
-
suggestedSkills: Array.from(new Set(
|
|
465
|
+
suggestedSkills: Array.from(new Set(suggestedSkills)),
|
|
452
466
|
actionGate: {
|
|
453
467
|
required: actionGateRequired,
|
|
454
468
|
reasonCodes: actionGateReasonCodes,
|
|
@@ -457,8 +471,9 @@ function buildWorkflowPlan(input = {}) {
|
|
|
457
471
|
},
|
|
458
472
|
limits: {
|
|
459
473
|
maxIterations: meta.maxIterations,
|
|
460
|
-
maxRevisionRounds:
|
|
461
|
-
|
|
474
|
+
maxRevisionRounds: WORKFLOW_MAX_REVISION_ROUNDS,
|
|
475
|
+
maxReplans: FABLE_MAX_REPLANS,
|
|
476
|
+
maxWorkers: FABLE_MAX_WORKERS,
|
|
462
477
|
},
|
|
463
478
|
parallelism: {
|
|
464
479
|
allowed: meta.parallelAllowed,
|
|
@@ -472,7 +487,7 @@ function buildWorkflowPlan(input = {}) {
|
|
|
472
487
|
memory: {
|
|
473
488
|
read: meta.memoryRead,
|
|
474
489
|
scope: 'task-relevant-only',
|
|
475
|
-
write:
|
|
490
|
+
write: memoryWrite,
|
|
476
491
|
},
|
|
477
492
|
verification: {
|
|
478
493
|
mode: meta.verificationMode,
|
|
@@ -553,6 +568,9 @@ function validateWorkflowPlan(plan) {
|
|
|
553
568
|
if (plan.actionGate && plan.actionGate.required && plan.actionGate.disposition !== 'pending-approval') errors.push('required actionGate must be pending-approval before execution');
|
|
554
569
|
if (!plan.limits || !Object.prototype.hasOwnProperty.call(plan.limits, 'maxIterations')) errors.push('limits.maxIterations is required');
|
|
555
570
|
if (plan.strategy === 'iterative-single' && (!Number.isInteger(plan.limits.maxIterations) || plan.limits.maxIterations < 1)) errors.push('iterative-single requires maxIterations');
|
|
571
|
+
if (!Number.isInteger(plan.limits?.maxRevisionRounds) || plan.limits.maxRevisionRounds < 0) errors.push('limits.maxRevisionRounds must be a non-negative integer');
|
|
572
|
+
if (!Number.isInteger(plan.limits?.maxReplans) || plan.limits.maxReplans < 0) errors.push('limits.maxReplans must be a non-negative integer');
|
|
573
|
+
if (!Number.isInteger(plan.limits?.maxWorkers) || plan.limits.maxWorkers < 1) errors.push('limits.maxWorkers must be a positive integer');
|
|
556
574
|
if (plan.strategy === 'fable-parallel' && (!plan.parallelism || plan.parallelism.allowed !== true)) errors.push('fable-parallel requires parallelism.allowed=true');
|
|
557
575
|
if (!plan.fallback || !['none', 'reduced', 'blocked'].includes(plan.fallback.disposition)) errors.push('fallback.disposition is invalid');
|
|
558
576
|
if (!Array.isArray(plan.reasonCodes)) errors.push('reasonCodes must be an array');
|
|
@@ -597,6 +615,9 @@ function writeRouterContract(contractPath, contract) {
|
|
|
597
615
|
module.exports = {
|
|
598
616
|
SCHEMA_VERSION,
|
|
599
617
|
ITERATIVE_MAX_ITERATIONS,
|
|
618
|
+
WORKFLOW_MAX_REVISION_ROUNDS,
|
|
619
|
+
FABLE_MAX_REPLANS,
|
|
620
|
+
FABLE_MAX_WORKERS,
|
|
600
621
|
buildRouterContract,
|
|
601
622
|
buildTaskShape,
|
|
602
623
|
buildWorkflowPlan,
|
|
@@ -74,6 +74,14 @@ function detectProhibitions(prompt) {
|
|
|
74
74
|
return rules.filter(([, regex]) => regex.test(prompt)).map(([name]) => name);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function detectMemoryPersistenceRequest(prompt) {
|
|
78
|
+
const text = String(prompt || '');
|
|
79
|
+
return /^\s*self-evolve\b/i.test(text) ||
|
|
80
|
+
/\b(?:run|use|invoke|execute)\s+(?:the\s+)?self-evolve\b/i.test(text) ||
|
|
81
|
+
/\b(?:persist|save|record|remember)\b.{0,40}\b(?:lesson|memory|rule|insight)\b/i.test(text) ||
|
|
82
|
+
/(?:執行|使用).{0,8}自我進化|(?:記住|保存|持久化|記錄).{0,20}(?:教訓|記憶|規則|經驗)/i.test(text);
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function detectActionGateReasons(prompt) {
|
|
78
86
|
const reasons = [];
|
|
79
87
|
const irreversible = [
|
|
@@ -226,6 +234,7 @@ function run(userPrompt, context) {
|
|
|
226
234
|
console.log(`[Tier Routing Pre-check]`);
|
|
227
235
|
|
|
228
236
|
const promptLower = userPrompt.toLowerCase();
|
|
237
|
+
const memoryPersistenceRequested = detectMemoryPersistenceRequest(userPrompt);
|
|
229
238
|
const loadedConfig = loadRoutingConfig();
|
|
230
239
|
const routingConfig = loadedConfig.config;
|
|
231
240
|
const TIER3_KEYWORDS = routingConfig.tiers.tier3 || [];
|
|
@@ -251,7 +260,12 @@ function run(userPrompt, context) {
|
|
|
251
260
|
let rationale = 'No structural/testing signals matched; unclassified is not equivalent to trivial.';
|
|
252
261
|
const reasonCodes = [...loadedConfig.reasonCodes, 'no-classification-signal'];
|
|
253
262
|
|
|
254
|
-
if (
|
|
263
|
+
if (memoryPersistenceRequested && !hasMacroSignal && !hasTier3Keyword && !hasTier2Keyword) {
|
|
264
|
+
recommendedTier = 'Tier 1 (Trivial)';
|
|
265
|
+
rationale = 'Explicit bounded self-evolve memory persistence request.';
|
|
266
|
+
reasonCodes.splice(loadedConfig.reasonCodes.length);
|
|
267
|
+
addReason(reasonCodes, 'memory-persistence-requested');
|
|
268
|
+
} else if (isTrivialDocsEdit) {
|
|
255
269
|
recommendedTier = 'Tier 1 (Trivial)';
|
|
256
270
|
rationale = 'Single documentation typo detected - direct edit, no checklist required.';
|
|
257
271
|
reasonCodes.splice(loadedConfig.reasonCodes.length);
|
|
@@ -409,11 +423,15 @@ function run(userPrompt, context) {
|
|
|
409
423
|
highUncertainty,
|
|
410
424
|
irreversibleAction,
|
|
411
425
|
externalSideEffect,
|
|
426
|
+
memoryPersistenceRequested,
|
|
412
427
|
highRisk: irreversibleAction || externalSideEffect,
|
|
413
428
|
},
|
|
414
429
|
});
|
|
415
430
|
|
|
416
431
|
console.log(`\n=> WORKFLOW STRATEGY: ${contract.workflowPlan.strategy || 'deferred'}`);
|
|
432
|
+
if (contract.workflowPlan.memory.write !== 'none') {
|
|
433
|
+
console.log(`=> MEMORY WRITE: ${contract.workflowPlan.memory.write}`);
|
|
434
|
+
}
|
|
417
435
|
if (contract.workflowPlan.actionGate.required) {
|
|
418
436
|
console.log(`=> ACTION GATE: required (${contract.workflowPlan.actionGate.reasonCodes.join(', ')}); disposition=pending-approval`);
|
|
419
437
|
}
|
package/hooks/hooks.json
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"id": "harness:pre:workflow-gate"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
"matcher": "Bash|PowerShell",
|
|
41
|
+
"matcher": "Bash|PowerShell|mcp__.*",
|
|
42
42
|
"hooks": [
|
|
43
43
|
{
|
|
44
44
|
"type": "command",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
],
|
|
139
139
|
"PostToolUse": [
|
|
140
140
|
{
|
|
141
|
-
"matcher": "Bash|PowerShell",
|
|
141
|
+
"matcher": "Bash|PowerShell|mcp__.*",
|
|
142
142
|
"hooks": [
|
|
143
143
|
{
|
|
144
144
|
"type": "command",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
],
|
|
207
207
|
"PostToolUseFailure": [
|
|
208
208
|
{
|
|
209
|
-
"matcher": "Bash|PowerShell",
|
|
209
|
+
"matcher": "Bash|PowerShell|mcp__.*",
|
|
210
210
|
"hooks": [
|
|
211
211
|
{
|
|
212
212
|
"type": "command",
|
|
@@ -16,6 +16,8 @@ const VALID_SCOPES = new Set(['always', 'outside-scratch']);
|
|
|
16
16
|
const POLICY_ENV = 'HARNESS_ACTION_GATE_POLICY';
|
|
17
17
|
const DEFER_POLICY = 'defer-to-host';
|
|
18
18
|
const ALWAYS_ASK_POLICY = 'always-ask';
|
|
19
|
+
const STRUCTURED_GATED_ACTIONS = new Set(['delete', 'publish', 'deploy', 'send', 'mutate']);
|
|
20
|
+
const STRUCTURED_SAFE_ACTIONS = new Set(['read']);
|
|
19
21
|
|
|
20
22
|
// Fail-safe defaults are used only when the configured table is missing or
|
|
21
23
|
// invalid. A valid-but-empty table stays empty so CI can catch policy erasure.
|
|
@@ -47,12 +49,113 @@ function toolInputOf(payload) {
|
|
|
47
49
|
return (payload && (payload.tool_input || payload.toolInput || payload.input)) || {};
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function stableValue(value) {
|
|
53
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
54
|
+
if (!value || typeof value !== 'object') return value;
|
|
55
|
+
return Object.keys(value).sort().reduce((acc, key) => {
|
|
56
|
+
acc[key] = stableValue(value[key]);
|
|
57
|
+
return acc;
|
|
58
|
+
}, {});
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
function exactPayloadText(payload) {
|
|
51
62
|
const input = toolInputOf(payload);
|
|
52
63
|
const tool = toolNameOf(payload);
|
|
53
64
|
if (tool === 'Bash' || tool === 'PowerShell') return String(input.command || '');
|
|
54
65
|
if (tool === 'apply_patch') return String(input.patch || input.command || input.content || '');
|
|
55
|
-
return JSON.stringify(input);
|
|
66
|
+
return JSON.stringify(stableValue(input));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function explicitActionDescriptor(payload) {
|
|
70
|
+
const input = toolInputOf(payload);
|
|
71
|
+
const candidates = [
|
|
72
|
+
payload && (payload.action_descriptor || payload.actionDescriptor),
|
|
73
|
+
input && (input.action_descriptor || input.actionDescriptor),
|
|
74
|
+
payload && payload.harness && (payload.harness.action_descriptor || payload.harness.actionDescriptor),
|
|
75
|
+
];
|
|
76
|
+
return candidates.find(value => value && typeof value === 'object' && !Array.isArray(value)) || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inferToolFamily(tool, explicit) {
|
|
80
|
+
if (explicit && ['shell', 'mcp', 'local-function'].includes(explicit.toolFamily)) return explicit.toolFamily;
|
|
81
|
+
if (tool === 'Bash' || tool === 'PowerShell') return 'shell';
|
|
82
|
+
if (/^mcp(?:__|_)/i.test(tool)) return 'mcp';
|
|
83
|
+
return 'local-function';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function inferStructuredAction(tool, explicit) {
|
|
87
|
+
const requested = explicit && String(explicit.action || '').toLowerCase();
|
|
88
|
+
if (['read', 'delete', 'publish', 'deploy', 'send', 'mutate', 'unknown'].includes(requested)) return requested;
|
|
89
|
+
const name = String(tool || '').toLowerCase();
|
|
90
|
+
if (/(?:^|[_-])(get|list|read|search|find|fetch|show|inspect)(?:$|[_-])/.test(name)) return 'read';
|
|
91
|
+
if (/(?:delete|remove|destroy|drop|purge)/.test(name)) return 'delete';
|
|
92
|
+
if (/(?:publish|release_create|create_release|release-publish)/.test(name)) return 'publish';
|
|
93
|
+
if (/(?:deploy|apply_production|promote)/.test(name)) return 'deploy';
|
|
94
|
+
if (/(?:send|email|message|notify|post_message)/.test(name)) return 'send';
|
|
95
|
+
if (/(?:create|update|write|edit|mutate|set_|add_|merge|close_)/.test(name)) return 'mutate';
|
|
96
|
+
return 'unknown';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function safeResource(input, explicit) {
|
|
100
|
+
const direct = explicit && explicit.resource;
|
|
101
|
+
if (typeof direct === 'string' && direct.trim()) return direct.trim().slice(0, 200);
|
|
102
|
+
for (const [key, value] of Object.entries(input || {})) {
|
|
103
|
+
if (/token|secret|password|credential|authorization|api[_-]?key/i.test(key)) continue;
|
|
104
|
+
if (!/resource|path|target|repo|repository|channel|issue|file|name|id$/i.test(key)) continue;
|
|
105
|
+
if (typeof value === 'string' || typeof value === 'number') return String(value).slice(0, 200);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeActionDescriptor(payload) {
|
|
111
|
+
const tool = toolNameOf(payload);
|
|
112
|
+
const input = toolInputOf(payload);
|
|
113
|
+
const explicit = explicitActionDescriptor(payload);
|
|
114
|
+
const toolFamily = inferToolFamily(tool, explicit);
|
|
115
|
+
const action = inferStructuredAction(tool, explicit);
|
|
116
|
+
const classification = STRUCTURED_GATED_ACTIONS.has(action)
|
|
117
|
+
? 'gated'
|
|
118
|
+
: (STRUCTURED_SAFE_ACTIONS.has(action) ? 'safe' : 'unknown');
|
|
119
|
+
const reasonCodes = [];
|
|
120
|
+
if (explicit) reasonCodes.push('explicit-structured-action');
|
|
121
|
+
else if (action !== 'unknown') reasonCodes.push('tool-identity-inference');
|
|
122
|
+
else reasonCodes.push('structured-action-unknown');
|
|
123
|
+
return {
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
toolFamily,
|
|
126
|
+
toolName: tool,
|
|
127
|
+
action,
|
|
128
|
+
resource: safeResource(input, explicit),
|
|
129
|
+
payloadHash: hashExact(exactPayloadText(payload)),
|
|
130
|
+
classification,
|
|
131
|
+
reasonCodes,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function structuredRule(payload) {
|
|
136
|
+
const descriptor = normalizeActionDescriptor(payload);
|
|
137
|
+
if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
|
|
138
|
+
if (descriptor.classification === 'safe') return { descriptor, rule: null };
|
|
139
|
+
if (descriptor.classification === 'gated') {
|
|
140
|
+
return {
|
|
141
|
+
descriptor,
|
|
142
|
+
rule: {
|
|
143
|
+
id: `structured-${descriptor.action}`,
|
|
144
|
+
reason: `Structured ${descriptor.action} action requires pre-action authorization.`,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const hint = routerActionGateHint(payload);
|
|
149
|
+
if (hint && hint.required) {
|
|
150
|
+
return {
|
|
151
|
+
descriptor,
|
|
152
|
+
rule: {
|
|
153
|
+
id: 'structured-unknown-required',
|
|
154
|
+
reason: 'The workflow requires an action gate, but this structured tool action is unknown.',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { descriptor, rule: null };
|
|
56
159
|
}
|
|
57
160
|
|
|
58
161
|
function validateRule(rule) {
|
|
@@ -306,11 +409,13 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
306
409
|
const exact = exactPayloadText(payload);
|
|
307
410
|
const commandHash = hashExact(exact);
|
|
308
411
|
const now = new Date().toISOString();
|
|
412
|
+
const descriptor = normalizeActionDescriptor(payload);
|
|
309
413
|
const record = {
|
|
310
414
|
schemaVersion: 1,
|
|
311
415
|
toolName: toolNameOf(payload),
|
|
312
416
|
toolUseId: (payload && (payload.tool_use_id || payload.toolUseId)) || null,
|
|
313
417
|
commandHash,
|
|
418
|
+
actionDescriptor: descriptor,
|
|
314
419
|
matchedRule: rule.id,
|
|
315
420
|
ruleReason: rule.reason,
|
|
316
421
|
disposition,
|
|
@@ -332,7 +437,8 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
332
437
|
function evaluatePreToolUse(payload, options = {}) {
|
|
333
438
|
const context = sessionContext(payload, options.sessionDir);
|
|
334
439
|
const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
|
|
335
|
-
const
|
|
440
|
+
const structured = structuredRule(payload);
|
|
441
|
+
const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
|
|
336
442
|
if (!rule) {
|
|
337
443
|
return {
|
|
338
444
|
kind: 'allow',
|
|
@@ -341,6 +447,7 @@ function evaluatePreToolUse(payload, options = {}) {
|
|
|
341
447
|
stderr: table.degraded ? `[Harness actionGate] ${table.degradationReason}; built-in defaults active.` : null,
|
|
342
448
|
rule: null,
|
|
343
449
|
table,
|
|
450
|
+
descriptor: structured.descriptor,
|
|
344
451
|
};
|
|
345
452
|
}
|
|
346
453
|
|
|
@@ -535,4 +642,6 @@ module.exports = {
|
|
|
535
642
|
routerActionGateHint,
|
|
536
643
|
scratchRoots,
|
|
537
644
|
validateRuleTable,
|
|
645
|
+
normalizeActionDescriptor,
|
|
646
|
+
structuredRule,
|
|
538
647
|
};
|
|
@@ -22,7 +22,7 @@ const {
|
|
|
22
22
|
listRunContracts,
|
|
23
23
|
getWorkerId,
|
|
24
24
|
} = require('./lib/fable-contracts');
|
|
25
|
-
const { loadWorkflow, matchingRun } = require('./lib/workflow-runtime');
|
|
25
|
+
const { loadWorkflow, matchingRun, recordBudgetEvent } = require('./lib/workflow-runtime');
|
|
26
26
|
|
|
27
27
|
function resultState(payload) {
|
|
28
28
|
const toolResponse = payload.tool_response || payload.tool_result || {};
|
|
@@ -140,6 +140,9 @@ process.stdin.on('end', () => {
|
|
|
140
140
|
const contract = updateRunContract(entry, payload, command, sessionId);
|
|
141
141
|
console.log(`[Contract Test] ${contract.planId}/${contract.runId}/${contract.stageId}: ${contract.status.toUpperCase()} ${contract.checkCommand}`);
|
|
142
142
|
if (contract.status === 'fail') {
|
|
143
|
+
if (workflowContext.workflow?.workflowId) {
|
|
144
|
+
recordBudgetEvent(workflowContext, 'revision', { evidence: `${contract.stageId}:${contract.evidence || 'verification-failed'}` });
|
|
145
|
+
}
|
|
143
146
|
console.error(`Evidence: ${contract.evidence || '(no output captured)'}`);
|
|
144
147
|
console.error(`Do not build on this stage's output until it passes. Artifact: ${contract.outputPath || '(no path recorded)'}`);
|
|
145
148
|
process.exit(2);
|