harness-everything 0.11.6 → 0.11.7
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 +13 -0
- package/README.md +7 -3
- package/ci/mechanism-2a-rule-of-3.test.js +71 -0
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +139 -0
- package/ci/mechanism-2c-state-persist.test.js +45 -2
- package/ci/mechanism-2t-issue-20.test.js +0 -8
- package/ci/mechanism-30-opencode-plugin-loadability.test.js +39 -7
- package/docs/architecture.md +17 -0
- package/docs/audit.md +52 -5
- package/docs/release-evidence.md +3 -0
- package/docs/workflow-runtime.md +13 -0
- package/docs/workflows/environment-detection.md +47 -37
- package/docs/workflows/eval-harness.md +45 -38
- package/docs/workflows/fable-discipline.md +45 -33
- package/docs/workflows/git-commit.md +53 -45
- package/docs/workflows/grill-me.md +36 -38
- package/docs/workflows/grill-with-docs.md +36 -46
- package/docs/workflows/harness-everything.md +59 -57
- package/docs/workflows/improve-codebase-architecture.md +36 -44
- package/docs/workflows/install-cognitive-os.md +36 -29
- package/docs/workflows/repo-docs.md +29 -40
- package/docs/workflows/rewrite-commits.md +40 -36
- package/docs/workflows/security-review.md +34 -53
- package/docs/workflows/skill-creator.md +39 -56
- package/docs/workflows/skill-style.md +35 -40
- package/docs/workflows/using-git-worktrees.md +40 -44
- package/docs/workflows/verification-loop.md +43 -48
- package/docs/workflows/verify-before-claim.md +37 -40
- package/docs/workflows/zoom-out.md +39 -51
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +38 -10
- package/hooks/scripts/lib/workflow-isolation.js +30 -1
- package/hooks/scripts/rule-of-3-tracker.js +17 -9
- package/hooks/scripts/state-persist.js +14 -6
- package/hooks/scripts/workflow-gate.js +5 -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/scripts/lib/workflow-isolation.js +30 -1
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +17 -9
- package/plugins/harness-everything/hooks/scripts/state-persist.js +14 -6
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +5 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +38 -10
- package/docs/issue-20-rollup.md +0 -47
- package/docs/reviews/pr-142-issue-131.md +0 -89
|
@@ -1,71 +1,66 @@
|
|
|
1
1
|
# Workflow: Verification Loop
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Run the objective gates — build, types, lint, tests, security, diff — and only claim done when the evidence-backed report passes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Source of truth: `verification-loop/SKILL.md`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Contract summary from SKILL.md — Trigger: completed implementation or change awaiting delivery. Output: evidence-backed verification report and delivery decision. USE FOR: verify a feature or change before delivery, run quality gates before a PR. DO NOT USE FOR: fixing failures without re-running the loop, writing new features or tests (use `tdd`).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 1. Skill Behavior Workflow
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
12
|
graph TD
|
|
13
|
-
Start
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
ScriptReport --> CheckGate{All Verification Gates Passed?}
|
|
25
|
-
DirectReport --> CheckGate
|
|
26
|
-
|
|
27
|
-
CheckGate -->|No| FixIssues["Fix Issues in Code & Re-verify"] --> RunBuild
|
|
28
|
-
CheckGate -->|Yes| End([Pre-PR Verification Gate Cleared])
|
|
13
|
+
Start[Completed Change Awaiting Delivery] --> Build[Gate1 Build npm run build]
|
|
14
|
+
Build --> Types[Gate2 Types tsc noEmit or pyright]
|
|
15
|
+
Types --> Lint[Gate3 Lint npm run lint or ruff]
|
|
16
|
+
Lint --> Tests[Gate4 Tests npm run test coverage]
|
|
17
|
+
Tests --> Security[Gate5 Security Secrets StrayLogs]
|
|
18
|
+
Security --> Diff[Gate6 Diff git diff stat]
|
|
19
|
+
Diff --> Report[Fill verification-report template]
|
|
20
|
+
Report --> Decision{All Gates Pass?}
|
|
21
|
+
Decision -->|yes| Deliver([Delivery Decision Ready])
|
|
22
|
+
Decision -->|no| Fix[Fix Cause Rerun From First Affected Gate]
|
|
23
|
+
Fix --> Build
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
---
|
|
32
|
-
|
|
33
26
|
## 2. Triggering and Routing Path
|
|
34
27
|
|
|
35
|
-
This diagram illustrates how the `verification-loop` skill is triggered through user requests or developer actions, and how it integrates or chains together with other companion skills in the Harness OS ecosystem to form unified workflows.
|
|
36
|
-
|
|
37
28
|
```mermaid
|
|
38
|
-
graph
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
graph TD
|
|
30
|
+
VerifyReq[Run Full Verification Loop Request] --> Skill[verification-loop SKILL]
|
|
31
|
+
DeliveryReq[Verify Before Delivery Request] --> Skill
|
|
32
|
+
PRReq[Quality Gates Before PR] --> Skill
|
|
33
|
+
DesignOnly[Design Discussion No Completed Work] --> Decline[Do Not Route]
|
|
34
|
+
PlanOnly[Planning Without Code Changes] --> Decline
|
|
35
|
+
NewWork[Write New Features Tests] --> TDD[tdd SKILL]
|
|
36
|
+
Skill --> Gates[Build Type Lint Test Security Diff]
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## 3. Real-World Use Case Flowchart
|
|
48
|
-
|
|
49
|
-
Here we model concrete real-world scenarios and use cases of the `verification-loop` skill, illustrating standard success paths, error handling, or recovery loops.
|
|
39
|
+
## 3. Real-World Use Case
|
|
50
40
|
|
|
51
41
|
```mermaid
|
|
52
42
|
graph TD
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
Done[Feature Implemented Awaiting PR] --> B[Run npm run build]
|
|
44
|
+
B -->|pass| T[Run npx tsc noEmit]
|
|
45
|
+
T -->|fail| FixT[Fix Type Errors]
|
|
46
|
+
FixT --> B
|
|
47
|
+
T -->|pass| L[Run npm run lint]
|
|
48
|
+
L -->|pass| TS[Run npm run test coverage]
|
|
49
|
+
TS -->|pass| Sec[Check Secrets And Stray Logs]
|
|
50
|
+
Sec --> D[Check git diff stat For Unintended Changes]
|
|
51
|
+
D --> R[Fill templates verification-report template md]
|
|
52
|
+
R --> PR[Report Evidence And Delivery Decision]
|
|
61
53
|
```
|
|
62
54
|
|
|
63
|
-
|
|
55
|
+
Template: `verification-loop/templates/verification-report.template.md`. Deep dive: `verification-loop/references/verification-phases.md`.
|
|
64
56
|
|
|
65
57
|
## 4. Verification Check
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- [ ]
|
|
70
|
-
- [ ]
|
|
71
|
-
- [ ]
|
|
59
|
+
- [ ] No completion was claimed until every applicable gate passed
|
|
60
|
+
- [ ] Build gate ran (`npm run build`) with evidence recorded
|
|
61
|
+
- [ ] Type gate ran (`npx tsc --noEmit` or `pyright .`) with evidence recorded
|
|
62
|
+
- [ ] Lint gate ran (`npm run lint` or `ruff check .`) with evidence recorded
|
|
63
|
+
- [ ] Test and coverage gate ran (`npm run test -- --coverage`) with evidence recorded
|
|
64
|
+
- [ ] Security gate checked secrets and stray logs with evidence recorded
|
|
65
|
+
- [ ] Diff gate checked `git diff --stat` and unintended changes with evidence recorded
|
|
66
|
+
- [ ] `templates/verification-report.template.md` was filled with evidence, and any failure was fixed then rerun from the first affected gate rather than patched without a rerun
|
|
@@ -1,64 +1,61 @@
|
|
|
1
1
|
# Workflow: Verify Before Claim
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Check external framework, SDK, CLI, and API claims plus any unmeasured performance or cost numbers against an authoritative source, then deliver a cited claim, a real measurement, or a labeled estimate.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Source of truth: `verify-before-claim/SKILL.md`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Contract summary from SKILL.md — Trigger: external behavior, version/config/exit-code claim, or unmeasured number. Output: official citation, real measurement, or explicitly labeled estimate. State mutation: none; evidence travels with the claim. Gate: official source or real measurement; unresolved claims stay inconclusive. USE FOR: stating how an external framework, SDK, CLI tool, or API behaves; quoting performance, cost, latency, or timing numbers; answering "does X support Y" or version-specific questions; citing defaults, exit codes, config flags, pricing, or rate limits. DO NOT USE FOR: claims about this repository's own code, facts the user supplied directly, generic CS knowledge such as Big-O.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 1. Skill Behavior Workflow
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
12
|
graph TD
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
EstimateFallback --> End
|
|
13
|
+
Claim[External Claim Or Unmeasured Number] --> Scope{Claim Scope?}
|
|
14
|
+
Scope -->|this repo own code| Local[Read Local Source Report Observed Facts]
|
|
15
|
+
Scope -->|external behavior or number| Official[Consult Official Docs First]
|
|
16
|
+
Official --> Numbers{Needs Number?}
|
|
17
|
+
Numbers -->|yes| Measure[Real Measurement Or Labeled Estimate]
|
|
18
|
+
Numbers -->|no| Cite[Return Cited Claim]
|
|
19
|
+
Measure --> Output[Claim With Evidence]
|
|
20
|
+
Cite --> Output
|
|
21
|
+
Local --> Output2[Fact From Source]
|
|
22
|
+
Official -->|contradiction or missing evidence| Inconclusive[Report Inconclusive]
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
---
|
|
27
|
-
|
|
28
25
|
## 2. Triggering and Routing Path
|
|
29
26
|
|
|
30
|
-
This diagram illustrates how the `verify-before-claim` skill is triggered through user requests or developer actions, and how it integrates or chains together with other companion skills in the Harness OS ecosystem to form unified workflows.
|
|
31
|
-
|
|
32
27
|
```mermaid
|
|
33
|
-
graph
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
graph TD
|
|
29
|
+
ExtBehave[State External Framework SDK CLI API Behavior] --> Skill[verify-before-claim SKILL]
|
|
30
|
+
PerfNum[Quote Perf Cost Latency Timing] --> Skill
|
|
31
|
+
SupportQ[Does X Support Y Version Question] --> Skill
|
|
32
|
+
ConfigQ[Cite Default ExitCode Flag Pricing Limit] --> Skill
|
|
33
|
+
OwnCode[Claim About This Repo Own Code] --> LocalSrc[Read Actual Source]
|
|
34
|
+
UserFact[Fact User Supplied Directly] --> Decline[Do Not Route]
|
|
35
|
+
GenericCS[Generic CS Knowledge BigO] --> Decline
|
|
36
|
+
Skill --> Cited[Cited Claim Or Measurement Or Estimate]
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## 3. Real-World Use Case Flowchart
|
|
39
|
+
## 3. Real-World Use Case
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
A developer is about to assert a version-specific SDK default and a latency number in a design note. The skill routes the default to official documentation for a citation, routes the latency claim to a real measurement, and labels the value an estimate when no run exists. If sources contradict each other or no authoritative source exists, the result is reported as inconclusive rather than asserted as certain, even under time pressure.
|
|
45
42
|
|
|
46
43
|
```mermaid
|
|
47
44
|
graph TD
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
UpdateCode --> Done([Potential production runtime crash prevented])
|
|
45
|
+
Draft[Draft Asserts SDK Default And Latency] --> CheckDocs[Check Official Docs For Default]
|
|
46
|
+
CheckDocs --> CheckNum[Require Real Measurement For Number]
|
|
47
|
+
CheckNum -->|measured| Cited2[Cited Default Plus Measured Number]
|
|
48
|
+
CheckNum -->|no run| Labeled[Labeled Estimate]
|
|
49
|
+
CheckDocs -->|conflict or gap| Inconc[Inconclusive Report]
|
|
54
50
|
```
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
Deep dive: `verify-before-claim/references/verification-guide.md`.
|
|
57
53
|
|
|
58
54
|
## 4. Verification Check
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- [ ]
|
|
63
|
-
- [ ]
|
|
64
|
-
- [ ]
|
|
56
|
+
- [ ] Local-repository claims were settled by reading the actual source, not by external citation
|
|
57
|
+
- [ ] External framework, SDK, CLI, or API behavior used official documentation first with a citation
|
|
58
|
+
- [ ] Performance, cost, latency, or timing numbers came from a real measurement or were explicitly labeled as estimates
|
|
59
|
+
- [ ] Defaults, exit codes, config flags, pricing, and rate limits were cited rather than asserted from memory
|
|
60
|
+
- [ ] Contradictions or missing evidence were reported as inconclusive
|
|
61
|
+
- [ ] No unverified external fact was claimed as certain, even under time pressure
|
|
@@ -1,74 +1,62 @@
|
|
|
1
1
|
# Workflow: Zoom Out
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Reflect-first breaker after three failures: stop edits, rebuild the full picture with read-only tools, write a fact-check report, then resume or escalate.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Source of truth: `zoom-out/SKILL.md`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Contract summary from SKILL.md — Trigger: three same-signature failures or an explicit loop/rethink request. Output: fact-checked report ending in `RESUME` or `ESCALATE`. State: writes the session `zoom-out-report.md`; reset may clear breaker state. Gate: `hooks/scripts/rule-of-3.js` plus a valid report; reset only after the second cycle. USE FOR: "you are stuck in a loop", "the same error keeps failing", "rethink your assumptions". DO NOT USE FOR: routine single-failure debugging, greenfield planning without failures.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 1. Skill Behavior Workflow
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
12
|
graph TD
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
DecisionGate -->|Requirement Conflict / Access Gap| Escalate["ESCALATE: Present 2-3 Options + Recommendation to Human"]
|
|
25
|
-
|
|
26
|
-
Resume --> Resolved{5. Problem Cracking Succeeds?}
|
|
27
|
-
Resolved -->|Yes| SelfEvolve["Call self-evolve to Persist Insight"]
|
|
28
|
-
Resolved -->|No: 3 More Failures| Escalate
|
|
29
|
-
SelfEvolve --> End([Reflection Completed & Lessons Persisted])
|
|
30
|
-
Escalate --> End
|
|
13
|
+
Third[Third Same Signature Failure] --> Stop[Stop Edits No User Ask Yet]
|
|
14
|
+
Stop --> ReadOnly[ReadOnly Restate Goal Check Files Config Logs]
|
|
15
|
+
ReadOnly --> Fill[Fill zoom-out-report template]
|
|
16
|
+
Fill --> Decide{RESUME or ESCALATE?}
|
|
17
|
+
Decide -->|untried path| Resume[RESUME On Untried Path]
|
|
18
|
+
Decide -->|genuine user decision| Escalate[ESCALATE With Options]
|
|
19
|
+
Resume --> Again{Repeated Breaker Cycle?}
|
|
20
|
+
Again -->|second cycle| Reset[npm run harness reset]
|
|
21
|
+
Reset --> Evolve[Record Insight With self-evolve]
|
|
22
|
+
Again -->|no| Done([Report Complete])
|
|
23
|
+
Escalate --> Done
|
|
31
24
|
```
|
|
32
25
|
|
|
33
|
-
---
|
|
34
|
-
|
|
35
26
|
## 2. Triggering and Routing Path
|
|
36
27
|
|
|
37
|
-
This diagram illustrates how the `zoom-out` skill is triggered through user requests or developer actions, and how it integrates or chains together with other companion skills in the Harness OS ecosystem to form unified workflows.
|
|
38
|
-
|
|
39
28
|
```mermaid
|
|
40
|
-
graph
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
graph TD
|
|
30
|
+
LoopReq[Stuck In Loop Request] --> Skill[zoom-out SKILL]
|
|
31
|
+
SameErr[Same Error Keeps Failing] --> Skill
|
|
32
|
+
Rethink[Rethink Assumptions Request] --> Skill
|
|
33
|
+
ThreeFail[Three Same Signature Failures] --> Breaker[hooks scripts rule-of-3 js]
|
|
34
|
+
Breaker --> Skill
|
|
35
|
+
SingleFail[Routine Single Failure] --> DirectFix[Fix Directly]
|
|
36
|
+
Greenfield[Greenfield Planning No Failures] --> Fable[fable-mode SKILL]
|
|
37
|
+
Skill --> ReportGate[Valid Report RESUME or ESCALATE]
|
|
45
38
|
```
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## 3. Real-World Use Case Flowchart
|
|
50
|
-
|
|
51
|
-
Here we model concrete real-world scenarios and use cases of the `zoom-out` skill, illustrating standard success paths, error handling, or recovery loops.
|
|
40
|
+
## 3. Real-World Use Case
|
|
52
41
|
|
|
53
42
|
```mermaid
|
|
54
43
|
graph TD
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
UserChoice --> Unlock["Unlock workspace and execute Option B"]
|
|
63
|
-
Unlock --> Done([Error loop successfully broken and resolved])
|
|
44
|
+
Fail[Same Test Fails Three Times Same Signature] --> Halt[Stop Edits Rebuild Picture ReadOnly]
|
|
45
|
+
Halt --> Inspect[Restate Goal Check Files Config Logs]
|
|
46
|
+
Inspect --> Write[Fill templates zoom-out-report template md]
|
|
47
|
+
Write --> Choice{Untried Path Exists?}
|
|
48
|
+
Choice -->|yes| Resume2[RESUME On Untried Path]
|
|
49
|
+
Choice -->|no| Esc2[ESCALATE With Options]
|
|
50
|
+
Resume2 -->|repeated cycle| Reset2[npm run harness reset Then self-evolve]
|
|
64
51
|
```
|
|
65
52
|
|
|
66
|
-
|
|
53
|
+
Template: `zoom-out/templates/zoom-out-report.template.md`. Enforcement: `hooks/scripts/rule-of-3.js`. Deep dive: `zoom-out/references/circuit-breaker.md`.
|
|
67
54
|
|
|
68
55
|
## 4. Verification Check
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- [ ]
|
|
73
|
-
- [ ]
|
|
74
|
-
- [ ]
|
|
57
|
+
- [ ] After three same-signature failures, edits stopped and the same approach was not retried
|
|
58
|
+
- [ ] Goal, files, configuration, and logs were rechecked with read-only tools before any resume
|
|
59
|
+
- [ ] `templates/zoom-out-report.template.md` was filled and the session `zoom-out-report.md` ends in `RESUME` or `ESCALATE`
|
|
60
|
+
- [ ] Resume targeted only an untried path; genuine user decisions were escalated with options
|
|
61
|
+
- [ ] Reset via `npm run harness:reset` happened only after the second cycle, with the insight recorded via `self-evolve`
|
|
62
|
+
- [ ] Routine single-failure debugging and greenfield planning without failures were not routed here
|
|
@@ -33,12 +33,26 @@ function issueMemoryCapability(workflow) {
|
|
|
33
33
|
return token;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Host-generated background-task notifications arrive as UserPromptSubmit
|
|
37
|
+
// hook payloads but are not user input (#168). Routing them like prompts
|
|
38
|
+
// replaces the workflow contract mid-turn (e.g. deferred -> a new Tier-2).
|
|
39
|
+
// A prompt that is entirely a notification envelope must never create or
|
|
40
|
+
// replace a workflow; the current contract is kept unchanged.
|
|
41
|
+
function isHostNotificationPrompt(prompt) {
|
|
42
|
+
const text = String(prompt || '').trim();
|
|
43
|
+
if (!text) return false;
|
|
44
|
+
return /^<task-notification[\s>][\s\S]*<\/task-notification>\s*$/i.test(text);
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
function persistWorkflow(plan, payload, prompt) {
|
|
37
48
|
if (!payload || !(payload.session_id || payload.sessionId)) return null;
|
|
38
49
|
const { runtime, hooksRoot } = loadRuntime();
|
|
39
50
|
const context = runtime.loadWorkflow(payload);
|
|
40
51
|
const { writeCurrentSession } = require(path.join(hooksRoot, 'lib/harness-state'));
|
|
41
52
|
writeCurrentSession(context.root, context.sessionId);
|
|
53
|
+
if (isHostNotificationPrompt(prompt)) {
|
|
54
|
+
return { ...context, hooksRoot, retained: true, hostNotification: true };
|
|
55
|
+
}
|
|
42
56
|
if (context.workflow && runtime.OPEN_STATES.has(context.workflow.state)) {
|
|
43
57
|
if (!context.workflow.workflowPlan) throw new Error('legacy unresolved workflow needs explicit migration; refusing to replace its obligations');
|
|
44
58
|
// Steering/status prompts cannot drop an unresolved execution contract.
|
|
@@ -92,17 +106,30 @@ function run(raw) {
|
|
|
92
106
|
try { payload = JSON.parse(raw); } catch (_) { console.error('[Workflow Router] Invalid hook payload; lifecycle persistence unavailable.'); }
|
|
93
107
|
}
|
|
94
108
|
const prompt = promptArg || (typeof payload?.prompt === 'string' ? payload.prompt : '');
|
|
95
|
-
const
|
|
96
|
-
|
|
109
|
+
const hostNotification = isHostNotificationPrompt(prompt);
|
|
110
|
+
// Host-generated notifications are lifecycle events, not user-authored task
|
|
111
|
+
// input. Never feed their text into the router: when there is an active
|
|
112
|
+
// contract we retain it below; when there is none, the event is a no-op.
|
|
113
|
+
const result = hostNotification ? null : core.route(promptArg, raw || null);
|
|
114
|
+
let plan = result?.contract?.workflowPlan || null;
|
|
97
115
|
let persisted = null;
|
|
98
116
|
let failure = null;
|
|
99
117
|
try {
|
|
100
118
|
persisted = persistWorkflow(plan, payload, prompt);
|
|
101
|
-
if (persisted?.retained) plan = persisted.workflow.workflowPlan;
|
|
119
|
+
if (persisted?.retained && persisted.workflow) plan = persisted.workflow.workflowPlan;
|
|
102
120
|
} catch (error) { failure = error; }
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
if (hostNotification && !persisted?.workflow) {
|
|
122
|
+
console.log('\n=> Host notification — no active execution contract; ignored for routing.');
|
|
123
|
+
if (failure) {
|
|
124
|
+
console.error('[Workflow Router] ' + failure.message);
|
|
125
|
+
process.exitCode = 2;
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (result?.sanitized) console.log(result.sanitized);
|
|
130
|
+
if (result?.stderr) process.stderr.write(result.stderr);
|
|
131
|
+
if (persisted?.hostNotification) console.log('\n=> Host notification — routing unchanged; keeping the active execution contract.');
|
|
132
|
+
else if (persisted?.retained) console.log('\n=> Retaining unresolved workflow from earlier prompts; the checkpoint below is the active execution contract.');
|
|
106
133
|
core.printWorkflowPlan(plan);
|
|
107
134
|
core.printRoutingCheckpoint(plan);
|
|
108
135
|
core.printKernelContract(plan);
|
|
@@ -116,7 +143,7 @@ function run(raw) {
|
|
|
116
143
|
console.log(' - Tier 3 / Fable source and artifact mutations require Git worktree isolation; unavailable isolation means BLOCKED.');
|
|
117
144
|
console.log(' - Escape is limited to workflow-uncovered-scope or host-capability-unavailable, with stage, scope and evidence; other obligations remain mandatory.');
|
|
118
145
|
}
|
|
119
|
-
if (persisted) {
|
|
146
|
+
if (persisted?.workflow) {
|
|
120
147
|
const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
|
|
121
148
|
console.log(' - Workflow id: ' + persisted.workflow.workflowId);
|
|
122
149
|
console.log(' - Memory write disposition: ' + (persisted.workflow.workflowPlan.memory?.write || 'none'));
|
|
@@ -130,12 +157,13 @@ function run(raw) {
|
|
|
130
157
|
if (failure) {
|
|
131
158
|
console.error('[Workflow Router] ' + failure.message);
|
|
132
159
|
process.exitCode = 2;
|
|
133
|
-
} else if (result.status !== 0) process.exitCode = result.status || 1;
|
|
160
|
+
} else if (result && result.status !== 0) process.exitCode = result.status || 1;
|
|
134
161
|
}
|
|
135
162
|
|
|
163
|
+
if (typeof module !== 'undefined') module.exports = { isHostNotificationPrompt };
|
|
164
|
+
|
|
136
165
|
if (process.argv.length > 2 || process.stdin.isTTY) run('');
|
|
137
|
-
else {
|
|
138
|
-
let input = '';
|
|
166
|
+
else { let input = '';
|
|
139
167
|
let finished = false;
|
|
140
168
|
const finish = () => { if (finished) return; finished = true; clearTimeout(timeout); run(input); };
|
|
141
169
|
const timeout = setTimeout(finish, 500);
|
|
@@ -248,6 +248,35 @@ function mutationPaths(payload) {
|
|
|
248
248
|
}
|
|
249
249
|
return [input.file_path || input.filePath || input.path].filter(value => typeof value === 'string' && value.trim());
|
|
250
250
|
}
|
|
251
|
+
|
|
252
|
+
// Bound workspace root plus its linked worktrees. Git failures (e.g. a
|
|
253
|
+
// non-git workspace) degrade to the root alone instead of throwing, so
|
|
254
|
+
// Tier-2 accounting never blocks on this enumeration (#165; cf. #162).
|
|
255
|
+
function worktreeRoots(root) {
|
|
256
|
+
const roots = [root];
|
|
257
|
+
try {
|
|
258
|
+
for (const field of git(root, ['worktree', 'list', '--porcelain', '-z']).split('\0')) {
|
|
259
|
+
if (field.startsWith('worktree ')) {
|
|
260
|
+
const candidate = field.slice('worktree '.length).trim();
|
|
261
|
+
if (candidate) roots.push(candidate);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch (_) { /* root alone */ }
|
|
265
|
+
return roots;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Direct-tool (Edit/Write/apply_patch) targets count against the Tier-2
|
|
269
|
+
// budget only when at least one target lies inside the workspace (#165).
|
|
270
|
+
// Unknown targets (no parseable path) stay conservative and count.
|
|
271
|
+
function directMutationInWorkspace(payload, cwd, root) {
|
|
272
|
+
const targets = mutationPaths(payload);
|
|
273
|
+
if (!targets.length) return true;
|
|
274
|
+
const roots = worktreeRoots(root);
|
|
275
|
+
return targets.some(target => {
|
|
276
|
+
const resolved = path.resolve(cwd, target);
|
|
277
|
+
return roots.some(candidate => within(candidate, resolved));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
251
280
|
function assertTargets(payload, cwd, isolatedRoot) {
|
|
252
281
|
const targets = mutationPaths(payload);
|
|
253
282
|
if (!targets.length) throw new Error('mutation target is unknown; cannot verify worktree isolation');
|
|
@@ -274,4 +303,4 @@ function assertShellScope(command, cwd, isolatedRoot) {
|
|
|
274
303
|
}
|
|
275
304
|
}
|
|
276
305
|
|
|
277
|
-
module.exports = { canonical, key, within, linkedWorktree, classifyShell, workspaceFingerprint, shellProbeKey, inputOf, cwdOf, commandOf, mutationPaths, assertTargets, assertShellScope };
|
|
306
|
+
module.exports = { canonical, key, within, linkedWorktree, classifyShell, workspaceFingerprint, shellProbeKey, inputOf, cwdOf, commandOf, mutationPaths, worktreeRoots, directMutationInWorkspace, assertTargets, assertShellScope };
|
|
@@ -17,6 +17,7 @@ process.stdin.on('end', () => {
|
|
|
17
17
|
const payload = JSON.parse(inputData);
|
|
18
18
|
const hookEventName = payload.hook_event_name || payload.hookEventName || '';
|
|
19
19
|
const isFailureEvent = hookEventName === 'PostToolUseFailure';
|
|
20
|
+
const isSuccessEvent = hookEventName === 'PostToolUse';
|
|
20
21
|
const toolResponse = payload.tool_response || {};
|
|
21
22
|
const stdout = toolResponse.stdout ?? payload.stdout ?? '';
|
|
22
23
|
const stderr = toolResponse.stderr ?? payload.stderr ?? '';
|
|
@@ -25,10 +26,10 @@ process.stdin.on('end', () => {
|
|
|
25
26
|
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
26
27
|
|
|
27
28
|
const explicitFailure = exitCode !== undefined && exitCode !== 0;
|
|
28
|
-
const explicitSuccess = exitCode === 0;
|
|
29
29
|
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
30
30
|
const looksLikeError = /\b(error|fail|failed|failure|exception|fatal|panic|traceback|denied|refused|cannot|unable)\b/i.test(stderr);
|
|
31
|
-
const isFailure = isFailureEvent || explicitFailure ||
|
|
31
|
+
const isFailure = isFailureEvent || explicitFailure ||
|
|
32
|
+
(!isSuccessEvent && exitCode === undefined && stderrSignal && looksLikeError);
|
|
32
33
|
const errorText = isFailureEvent
|
|
33
34
|
? (failureText || (stderrSignal ? stderr : stdout) || '')
|
|
34
35
|
: ((stderrSignal ? stderr : stdout) || failureText || '');
|
|
@@ -47,19 +48,22 @@ process.stdin.on('end', () => {
|
|
|
47
48
|
const filePath = payload.tool_input?.file_path || payload.tool_input?.filePath || '';
|
|
48
49
|
const command = payload.tool_input?.command || '';
|
|
49
50
|
|
|
50
|
-
// Categorize failure type for context-aware thresholds
|
|
51
|
+
// Categorize failure type for context-aware thresholds.
|
|
52
|
+
// NOTE (#167): match anchored diagnostic phrases, not bare substrings.
|
|
53
|
+
// A bare `denied`/`timeout` substring matches file names such as
|
|
54
|
+
// `workflow-mutation-denied.js` and misclassifies the failure.
|
|
51
55
|
let category = 'unknown';
|
|
52
|
-
if (
|
|
56
|
+
if (/\bSyntaxError\b|Unexpected token|Syntax Error/i.test(errorText)) {
|
|
53
57
|
category = 'syntax';
|
|
54
|
-
} else if (
|
|
58
|
+
} else if (/\bENOENT\b|\bnot found\b|No such file|找不到|不存在/i.test(errorText)) {
|
|
55
59
|
category = 'environment';
|
|
56
|
-
} else if (
|
|
60
|
+
} else if (/\bETIMEDOUT\b|TimeoutError|\btimeout\b|\btimed out\b|\btiming out\b|超時|逾時/i.test(errorText)) {
|
|
57
61
|
category = 'timeout';
|
|
58
62
|
} else if (/test.*fail|assertion|AssertionError|測試.*失敗|斷言.*錯誤/i.test(errorText)) {
|
|
59
63
|
category = 'test';
|
|
60
|
-
} else if (
|
|
64
|
+
} else if (/\bpermission denied\b|\bEACCES\b|\bEPERM\b|權限.*(拒絕|不足|被拒)|拒絕.*權限/i.test(errorText)) {
|
|
61
65
|
category = 'permission';
|
|
62
|
-
} else if (
|
|
66
|
+
} else if (/\bdependency\b|\bmodule not found\b|Cannot find module|模組.*找不到/i.test(errorText)) {
|
|
63
67
|
category = 'dependency';
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -114,7 +118,11 @@ process.stdin.on('end', () => {
|
|
|
114
118
|
state.lastFailureAt = Date.now();
|
|
115
119
|
|
|
116
120
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
117
|
-
} else if (!
|
|
121
|
+
} else if (!isFailure) {
|
|
122
|
+
// NOTE (#166, follows #153): Claude Code PostToolUse success payloads
|
|
123
|
+
// carry no numeric exit code. The lifecycle event is authoritative:
|
|
124
|
+
// PostToolUse is success unless a numeric non-zero status contradicts it;
|
|
125
|
+
// hosts without lifecycle event names keep the conservative stderr fallback.
|
|
118
126
|
// A successful action after an accepted zoom-out is an objective recovery
|
|
119
127
|
// boundary. Emit only IDs/hashes/category; never copy command/output text.
|
|
120
128
|
if ((state.zoomOutCycles || 0) > 0 && state.lastHash) {
|
|
@@ -3,7 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
5
5
|
const { loadWorkflow, saveWorkflow, peekMutationProbe, settleMutationProbe } = require('./lib/workflow-runtime');
|
|
6
|
-
const { cwdOf, shellProbeKey, workspaceFingerprint } = require('./lib/workflow-isolation');
|
|
6
|
+
const { cwdOf, shellProbeKey, workspaceFingerprint, directMutationInWorkspace } = require('./lib/workflow-isolation');
|
|
7
7
|
const { observeTool } = require('./lib/telemetry');
|
|
8
8
|
|
|
9
9
|
// Commands that count as "verification ran" for the Stop gate
|
|
@@ -79,9 +79,10 @@ function processState(payload) {
|
|
|
79
79
|
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
80
80
|
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
81
81
|
const hookEvent = payload.hook_event_name || payload.hookEventName || '';
|
|
82
|
+
const isSuccessEvent = hookEvent === 'PostToolUse';
|
|
82
83
|
const isFailed = hookEvent === 'PostToolUseFailure' ||
|
|
83
84
|
(exitCode !== undefined && exitCode !== 0) ||
|
|
84
|
-
(exitCode === undefined && stderrSignal);
|
|
85
|
+
(!isSuccessEvent && exitCode === undefined && stderrSignal);
|
|
85
86
|
const toolName = payload.tool_name || payload.tool || 'command';
|
|
86
87
|
const observedMutation = observeWorkspaceMutation(payload, root);
|
|
87
88
|
|
|
@@ -97,8 +98,13 @@ function processState(payload) {
|
|
|
97
98
|
state.tool = toolName;
|
|
98
99
|
state.exitCode = exitCode;
|
|
99
100
|
state.errorSummary = output.trim();
|
|
100
|
-
} else if (
|
|
101
|
-
// Clear failed state or mark as idle/resolved
|
|
101
|
+
} else if (!isFailed && state.status === 'failed') {
|
|
102
|
+
// Clear failed state or mark as idle/resolved.
|
|
103
|
+
// NOTE (#166, follows #153): Claude Code Bash PostToolUse payloads
|
|
104
|
+
// carry no numeric exit code, so `exitCode === 0` never holds there.
|
|
105
|
+
// A PostToolUse event with no numeric non-zero exit status counts as
|
|
106
|
+
// success even when stderr contains diagnostics. Hosts that omit the
|
|
107
|
+
// lifecycle event name retain the conservative stderr fallback.
|
|
102
108
|
state.lastResolved = { tool: state.tool, timestamp: state.timestamp };
|
|
103
109
|
state.status = 'idle';
|
|
104
110
|
state.timestamp = new Date().toISOString();
|
|
@@ -107,8 +113,10 @@ function processState(payload) {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
// Milestones for the Stop gate: when did the last mutation happen, and
|
|
110
|
-
// has any verification-ish command succeeded since.
|
|
111
|
-
|
|
116
|
+
// has any verification-ish command succeeded since. Direct-tool edits
|
|
117
|
+
// outside the workspace advance neither milestone (#165).
|
|
118
|
+
const isDirectTool = toolName === 'Edit' || toolName === 'Write' || toolName === 'apply_patch';
|
|
119
|
+
if ((isDirectTool && directMutationInWorkspace(payload, cwdOf(payload, root), root)) || observedMutation) {
|
|
112
120
|
state.lastEditAt = Date.now();
|
|
113
121
|
}
|
|
114
122
|
if ((toolName === 'Bash' || toolName === 'PowerShell') && !isFailed) {
|