@shahboura/harness 3.0.4

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.
Files changed (62) hide show
  1. package/.opencode/agents/developer.md +60 -0
  2. package/.opencode/agents/planner.md +51 -0
  3. package/.opencode/agents/reviewer.md +53 -0
  4. package/.opencode/commands/add-repo.md +26 -0
  5. package/.opencode/commands/dev-workflow.md +40 -0
  6. package/.opencode/commands/init-workspace.md +22 -0
  7. package/.opencode/commands/migrate-workspace.md +28 -0
  8. package/.opencode/commands/repo-map-refresh.md +23 -0
  9. package/.opencode/commands/story-workflow.md +24 -0
  10. package/.opencode/commands/workflow-status.md +23 -0
  11. package/.opencode/commands/workspace-config.md +27 -0
  12. package/.opencode/package.json +9 -0
  13. package/.opencode/plugins/harness-guards.ts +290 -0
  14. package/.opencode/skills/add-repo/SKILL.md +96 -0
  15. package/.opencode/skills/dev-workflow/SKILL.md +105 -0
  16. package/.opencode/skills/dev-workflow/shared/diagram-styling.md +65 -0
  17. package/.opencode/skills/dev-workflow/shared/engineering.md +14 -0
  18. package/.opencode/skills/dev-workflow/shared/status-block.md +29 -0
  19. package/.opencode/skills/dev-workflow/steps/analyze-comments.md +32 -0
  20. package/.opencode/skills/dev-workflow/steps/apply-fixes.md +24 -0
  21. package/.opencode/skills/dev-workflow/steps/comment-analysis.md +11 -0
  22. package/.opencode/skills/dev-workflow/steps/create-pr.md +39 -0
  23. package/.opencode/skills/dev-workflow/steps/develop-task.md +54 -0
  24. package/.opencode/skills/dev-workflow/steps/develop.md +60 -0
  25. package/.opencode/skills/dev-workflow/steps/fetch.md +34 -0
  26. package/.opencode/skills/dev-workflow/steps/fixup-task.md +12 -0
  27. package/.opencode/skills/dev-workflow/steps/gate.md +53 -0
  28. package/.opencode/skills/dev-workflow/steps/harden-task.md +12 -0
  29. package/.opencode/skills/dev-workflow/steps/harden.md +30 -0
  30. package/.opencode/skills/dev-workflow/steps/intake.md +52 -0
  31. package/.opencode/skills/dev-workflow/steps/metrics.md +21 -0
  32. package/.opencode/skills/dev-workflow/steps/plan-task.md +108 -0
  33. package/.opencode/skills/dev-workflow/steps/plan.md +80 -0
  34. package/.opencode/skills/dev-workflow/steps/pre-pr-fixes.md +16 -0
  35. package/.opencode/skills/dev-workflow/steps/pre-pr-review.md +14 -0
  36. package/.opencode/skills/dev-workflow/steps/pre-pr.md +14 -0
  37. package/.opencode/skills/dev-workflow/steps/preflight.md +32 -0
  38. package/.opencode/skills/dev-workflow/steps/quick-recheck.md +14 -0
  39. package/.opencode/skills/dev-workflow/steps/reconcile.md +27 -0
  40. package/.opencode/skills/dev-workflow/steps/review-task.md +32 -0
  41. package/.opencode/skills/dev-workflow/steps/security.md +18 -0
  42. package/.opencode/skills/dev-workflow/steps/triage-request.md +15 -0
  43. package/.opencode/skills/init-workspace/SKILL.md +155 -0
  44. package/.opencode/skills/migrate-workspace/SKILL.md +122 -0
  45. package/.opencode/skills/repo-map-refresh/SKILL.md +43 -0
  46. package/.opencode/skills/story-workflow/SKILL.md +78 -0
  47. package/.opencode/skills/story-workflow/commands/analyze.md +40 -0
  48. package/.opencode/skills/story-workflow/commands/groom.md +58 -0
  49. package/.opencode/skills/story-workflow/commands/improve.md +47 -0
  50. package/.opencode/skills/story-workflow/commands/refine.md +44 -0
  51. package/.opencode/skills/story-workflow/shared/context.md +22 -0
  52. package/.opencode/skills/story-workflow/shared/provider-io.md +73 -0
  53. package/.opencode/skills/story-workflow/templates/readiness-report.md +57 -0
  54. package/.opencode/skills/story-workflow/templates/story-template.md +51 -0
  55. package/.opencode/skills/story-workflow/templates/technical-notes.md +48 -0
  56. package/.opencode/skills/workflow-status/SKILL.md +30 -0
  57. package/.opencode/skills/workspace-config/SKILL.md +97 -0
  58. package/.opencode/tsconfig.json +11 -0
  59. package/CHANGELOG.md +396 -0
  60. package/LICENSE +21 -0
  61. package/README.md +351 -0
  62. package/package.json +32 -0
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: "Implementation agent with proof-anchored TDD enforcement"
3
+ mode: "subagent"
4
+ hidden: true
5
+ model: "anthropic/claude-sonnet-4-5"
6
+ permission:
7
+ read: allow
8
+ grep: allow
9
+ glob: allow
10
+ write:
11
+ "core/**": "allow"
12
+ "tests/**": "allow"
13
+ "*": "ask"
14
+ edit:
15
+ "core/**": "allow"
16
+ "tests/**": "allow"
17
+ "*": "ask"
18
+ bash: allow
19
+ task: deny
20
+ ---
21
+
22
+ # Developer Agent
23
+
24
+ You are the **developer** agent for the ai-sdlc-harness SDLC pipeline.
25
+
26
+ ## Responsibilities
27
+ - Write failing tests first (proven-red TDD)
28
+ - Implement code to make tests pass
29
+ - Harden implementation (security, edge cases)
30
+ - Apply pre-PR fixes from reviewer feedback
31
+
32
+ ## Spawn Headers
33
+ Your spawn prompt carries structured headers:
34
+ - `harness-mode`: develop | harden | fixup
35
+ - `harness-task`: task-id
36
+ - `harness-run`: run-dir
37
+ - `harness-repo`: worktree-path
38
+ - `harness-test-cmd`: test command
39
+
40
+ ## Path Confinement (Plugin-Enforced)
41
+ - Work ONLY inside `harness-repo` (your worktree). Never touch `ai/<run>/` authority files.
42
+ - State moves only via `harness` commands, never by direct file writes.
43
+ - Non-test paths are blocked until `bin/harness verify-red` seals red-proof.
44
+
45
+ ## TDD Enforcement (Plugin-Enforced)
46
+ 1. Write tests first — non-test writes blocked until `bin/harness verify-red` seals red-proof
47
+ 2. `bin/harness verify-red` runs tests, seals chained red-proof + SHA-locks test files
48
+ 3. Implement until green; checkpoint via `bin/harness commit`
49
+ 4. Completion requires `verify-green` + red-proof SHA verification
50
+ 5. Test revisions require `bin/harness verify-red --revise --reason "..."` (flagged event)
51
+
52
+ ## Hard Rules
53
+ - Never run raw `git commit` / `merge` / `rebase` — use `bin/harness commit`
54
+ - Cite `.opencode/skills/dev-workflow/shared/engineering.md` for code standards
55
+ - Near turn ceiling: `bin/harness commit --commit-class wip` a checkpoint, then report `harness-status: PARTIAL`
56
+ - End EVERY response with the status block (`.opencode/skills/dev-workflow/shared/status-block.md`)
57
+
58
+ ## Worktree Isolation
59
+ - Each task runs in dedicated git worktree: `bin/harness worktree-add --task-id T1`
60
+ - Worktree removed on task completion: `bin/harness worktree-remove`
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: "Planning & analysis agent for SDLC workflow — orchestrates the pipeline"
3
+ mode: "primary"
4
+ model: "anthropic/claude-sonnet-4-5"
5
+ permission:
6
+ read: allow
7
+ grep: allow
8
+ glob: allow
9
+ write:
10
+ "ai/*": "allow"
11
+ ".claude/context/*": "allow"
12
+ "*": "deny"
13
+ edit:
14
+ "ai/*": "allow"
15
+ ".claude/context/*": "allow"
16
+ "*": "deny"
17
+ bash: allow
18
+ task:
19
+ "developer": "allow"
20
+ "reviewer": "allow"
21
+ "*": "deny"
22
+ webfetch: allow
23
+ ---
24
+
25
+ # Planner Agent
26
+
27
+ You are the **planner** agent for the ai-sdlc-harness SDLC pipeline.
28
+
29
+ ## Responsibilities
30
+ - **Intake**: Fetch and classify work items from providers (GitHub, GitLab, ADO, Jira, local markdown)
31
+ - **Plan**: Create detailed implementation plans with test intents, risk tiers, edge cases
32
+ - **Repo Map**: Generate and refresh codebase maps for grounding
33
+ - **Orchestrate**: Coordinate developer and reviewer subagents through the pipeline
34
+
35
+ ## Path Confinement (Plugin-Enforced)
36
+ - Writes allowed only under `ai/<run>/` and `.claude/context/`
37
+ - Never write to repo source files directly
38
+ - All git operations via `bin/harness` CLI verbs
39
+ - State managed via `bin/harness` state machine
40
+
41
+ ## Workflow
42
+ 1. Receive work item via `/dev-workflow <id>` or `/story-workflow analyze <id>`
43
+ 2. Run intake → plan → (human gate) → preflight
44
+ 3. Spawn developer via `task` tool (spawn prompt should ideally include `harness-mode: develop` for traceability; **note: opencode plugin cannot validate these headers** — spawn permission is enforced at agent level via `permission.task` globs, not prompt content)
45
+ 4. Spawn reviewer via `task` tool (same advisory header pattern)
46
+ 5. Coordinate review, reconciliation, and gates
47
+
48
+ ## Mode Reference
49
+ - `intake` → read `<run>/work-item.json`, produce requirements summary in `<run>/requirements.md`
50
+ - `plan` → `.opencode/skills/dev-workflow/steps/plan-task.md` — decomposition, two-altitude approach selection, test-intents, diagrams
51
+ - `repo-map` → generate tiered repo map under `.claude/context/repo-map/`
@@ -0,0 +1,53 @@
1
+ ---
2
+ description: "Code review & security analysis agent (read-only)"
3
+ mode: "subagent"
4
+ hidden: true
5
+ model: "anthropic/claude-sonnet-4-5"
6
+ permission:
7
+ read: allow
8
+ grep: allow
9
+ glob: allow
10
+ write: deny
11
+ edit: deny
12
+ bash:
13
+ "*": "ask"
14
+ "npm test*": "allow"
15
+ "python -m unittest*": "allow"
16
+ "cat /tmp/*": "allow"
17
+ task: deny
18
+ ---
19
+
20
+ # Reviewer Agent
21
+
22
+ You are the **reviewer** agent for the ai-sdlc-harness SDLC pipeline.
23
+
24
+ ## Responsibilities
25
+ - Review implementation diffs independently (re-run build + tests)
26
+ - Security scanning (configured scan commands)
27
+ - Pre-PR review (completeness, contracts, docs)
28
+ - PR comment analysis & triage
29
+ - Request triage (ad-hoc human requests during runs)
30
+
31
+ ## Path Confinement (Plugin-Enforced)
32
+ - **Strictly read-only**: No write/edit access granted
33
+ - Bash commands restricted: test runners and `/tmp` reads allowed; shell writes blocked
34
+ - `bin/harness` verbs allowed for state queries
35
+ - Never trust another agent's claim — verify independently by re-running builds and tests
36
+
37
+ ## Review Modes
38
+ - `review`: Per-task diff review inside develop mode. Verdict: APPROVED or CHANGES_REQUESTED with numbered, severity-tagged findings. Re-run build/tests yourself.
39
+ - `pre-pr`: Holistic pre-PR review producing `<run>/reports/pre-pr.md`
40
+ - `analyze-comments`: Classify PR comments VALID / INVALID / PARTIAL
41
+ - `request-triage`: Triage ad-hoc human request against the plan
42
+
43
+ ## Verdict Format
44
+ Output structured verdict captured by hooks:
45
+ ```
46
+ VERDICT: APPROVED
47
+ # or
48
+ VERDICT: CHANGES_REQUESTED
49
+ 1. Finding description (severity: high|medium|low)
50
+ 2. Finding description
51
+ ```
52
+
53
+ End every response with the status block (`.opencode/skills/dev-workflow/shared/status-block.md`).
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: "Register one new repo into an already-bootstrapped workspace"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /add-repo
8
+
9
+ Register a new repository into an existing workspace without re-running the full setup.
10
+
11
+ ## Usage
12
+ ```
13
+ /add-repo --name <name> --path <path> [--test-cmd <cmd>]
14
+ ```
15
+
16
+ ## Arguments
17
+ - `--name <name>` — Short name for the repo
18
+ - `--path <path>` — Absolute path to the cloned repo
19
+ - `--test-cmd <cmd>` — Test command (auto-discovered if omitted)
20
+
21
+ ## Workflow
22
+ 1. `bin/harness discover --repo <path>` — Auto-detect toolchain
23
+ 2. Review discovered settings
24
+ 3. `bin/harness add-repo --name <n> --path <path> --test-cmd <cmd>`
25
+ 4. `bin/harness init-verify` — Verification gate
26
+ 5. `bin/harness init-finalize` — Finalize configuration
@@ -0,0 +1,40 @@
1
+ ---
2
+ description: "Execute full SDLC workflow for a work item: fetch → plan → TDD → review → security → PR → reconcile → metrics"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /dev-workflow
8
+
9
+ Execute the complete governed SDLC pipeline for a work item end-to-end.
10
+
11
+ ## Usage
12
+ ```
13
+ /dev-workflow <work-item-id>
14
+ ```
15
+
16
+ ## Arguments
17
+ - `work-item-id`: Provider work item ID (e.g., `PROJ-123`, `GH-456`)
18
+
19
+ ## Workflow
20
+ 1. **Fetch & Classify** — Retrieve work item, classify mode (full/quick)
21
+ 2. **Intake** — Extract requirements, acceptance criteria
22
+ 3. **Plan** — Create detailed plan with test intents, risk tiers, diagrams
23
+ 4. **Human Gate: Approve Plan** — You approve/reject the plan
24
+ 5. **Preflight** — Verify toolchain, create worktree
25
+ 6. **Develop** — Proven-red TDD per task (developer agent)
26
+ 7. **Human Gate: Approve Implementation** — You approve/reject
27
+ 8. **Harden** — Security scan, edge cases
28
+ 9. **Security Gate** — Conditional (threshold-based)
29
+ 10. **Pre-PR Review** — Reviewer agent checks diff
30
+ 11. **Human Gate: Approve Pre-PR** — You approve/reject
31
+ 12. **Create PR** — `bin/harness create-pr` + `bin/harness push`
32
+ 13. **Analyze Comments** — Optional: reviewer triages PR comments
33
+ 14. **Reconcile** — Apply fixes, update contracts
34
+ 15. **Metrics** — Record token spend, cycle time, gate outcomes
35
+
36
+ ## Quick Mode
37
+ If work item has `Mode: quick` hint and no risk keywords:
38
+ - Skips plan step, single pre-PR gate
39
+ - Size caps: 80 lines / 5 files (configurable)
40
+ - `quick-recheck` escalates to full mode if diff exceeds caps
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: "One-time workspace setup: provider, repos, toolchain discovery, verification gate"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /init-workspace
8
+
9
+ One-time interview that bootstraps a workspace for the ai-sdlc-harness pipeline. Creates `.claude/context/`, discovers toolchain, configures provider, registers repos, and runs a verification gate.
10
+
11
+ ## Usage
12
+ ```
13
+ /init-workspace
14
+ ```
15
+
16
+ ## Workflow
17
+ 1. **Provider selection** — GitHub, GitLab, ADO, Jira, or local-markdown
18
+ 2. **Repo registration** — point to cloned repos, discover test commands
19
+ 3. **Toolchain discovery** — detect language, test framework, build tools
20
+ 4. **Verification gate** — every check must pass before proceeding
21
+ 5. **Permission config** — writes `.claude/settings.json` with allowlist
22
+ 6. **Bootstrap marker** — marks workspace as initialized
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: "Adopt a v2.x workspace: config carries over, run history stays archived"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /migrate-workspace
8
+
9
+ Migrate an existing v2.x workspace to the v3.0 format. Configuration carries over; run history stays archived in place.
10
+
11
+ ## Usage
12
+ ```
13
+ /migrate-workspace [--path <path>]
14
+ ```
15
+
16
+ ## Arguments
17
+ - `--path <path>` — Path to existing v2.x workspace (defaults to current directory)
18
+
19
+ ## Workflow
20
+ 1. `bin/harness migrate-detect` — Detect v2.x workspace structure
21
+ 2. Review detected settings
22
+ 3. `bin/harness migrate-extract` — Extract and convert configuration
23
+ 4. Verify migrated workspace
24
+ 5. Archive old run history
25
+
26
+ ## Notes
27
+ - Old run data is not deleted — only archived
28
+ - Permission settings are migrated but may need review for opencode format
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: "Regenerate the auto-generated codebase map the planner grounds its plans in"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /repo-map-refresh
8
+
9
+ Regenerate the codebase map used by the planner for grounding implementation plans.
10
+
11
+ ## Usage
12
+ ```
13
+ /repo-map-refresh [--repo-name <name>] [--repo <path>]
14
+ ```
15
+
16
+ ## Arguments
17
+ - `--repo-name <name>` — Name of the repo to map (omitted for all repos)
18
+ - `--repo <path>` — Path to the repo
19
+
20
+ ## Workflow
21
+ 1. `bin/harness repo-map-check --repo-name <n> --repo <path>` — Check staleness
22
+ 2. Generate tiered map (structure → key files → details)
23
+ 3. `bin/harness repo-map-stamp --repo-name <n> --repo <path>` — Stamp freshness
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: "Shape a story's quality before building: analyze, refine, improve, groom"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /story-workflow
8
+
9
+ Analyze and shape a work item's quality before implementation begins.
10
+
11
+ ## Usage
12
+ ```
13
+ /story-workflow <command> <work-item-id>
14
+ ```
15
+
16
+ ## Commands
17
+ - `analyze <id>` — Analyze work item for clarity, completeness, testability
18
+ - `refine <id>` — Refine acceptance criteria, add edge cases
19
+ - `improve <id>` — Improve test intents, risk assessment
20
+ - `groom <id>` — Full grooming pass: dependencies, sizing, priority
21
+
22
+ ## Arguments
23
+ - `command`: One of `analyze`, `refine`, `improve`, `groom`
24
+ - `work-item-id`: Provider work item ID
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: "Read-only dashboard: cursor, tasks, gates, flagged events per run"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /workflow-status
8
+
9
+ Read-only dashboard showing current pipeline state for one or all runs.
10
+
11
+ ## Usage
12
+ ```
13
+ /workflow-status [--run <run-id>]
14
+ ```
15
+
16
+ ## Options
17
+ - `--run <run-id>` — Show status for a specific run (omit for all runs)
18
+
19
+ ## Output
20
+ - Current step and mode
21
+ - Task statuses (pending / in-progress / in-review / done)
22
+ - Gate states (pending / approved / rejected)
23
+ - Flagged events and metrics
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: "Change one config section without re-running the interview"
3
+ agent: planner
4
+ model: anthropic/claude-sonnet-4-5
5
+ ---
6
+
7
+ # /workspace-config
8
+
9
+ Modify workspace configuration sections without re-running the full setup interview.
10
+
11
+ ## Usage
12
+ ```
13
+ /workspace-config <section> [<key>=<value> ...]
14
+ ```
15
+
16
+ ## Sections
17
+ - `provider` — Change provider settings
18
+ - `repos` — Update repo paths or test commands
19
+ - `workflow` — Adjust pipeline knobs (review rounds, security thresholds)
20
+ - `naming` — Branch/commit naming conventions
21
+ - `permissions` — Update permission allowlists
22
+
23
+ ## Examples
24
+ ```
25
+ /workspace-config workflow review_rounds.max=3
26
+ /workspace-config naming branch_prefix=feat
27
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@opencode-ai/plugin": "^1.17.0"
5
+ },
6
+ "devDependencies": {
7
+ "@types/node": "^26.1.1"
8
+ }
9
+ }
@@ -0,0 +1,290 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+
3
+ // ── Types ────────────────────────────────────────────────────────────────
4
+
5
+ interface PermissionEvent {
6
+ type: string;
7
+ command?: string;
8
+ path?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ // ── Python guard pattern equivalents (hooks/guards.py) ──────────────────
13
+
14
+ /**
15
+ * GIT_VERB_RE equivalent — matches `git commit|merge|push|...` with global
16
+ * flags between `git` and the verb, anchored at command boundaries and
17
+ * excluded inside quotes.
18
+ *
19
+ * Python source: hooks/guards.py lines 130-134
20
+ *
21
+ * Breakdown:
22
+ * (?<!['"]) – negative lookbehind: NOT preceded by quote
23
+ * \bgit\b – word-boundary git
24
+ * (?:[ \t]+ – optional global flags (zero or more):
25
+ * (?:-C|-c|--git-dir|…)(?:=TOKEN|[ \t]+TOKEN)? – flags that take a value
26
+ * |-{1,2}[A-Za-z][\w-]*(?:=TOKEN)? – self-contained flags
27
+ * )*
28
+ * [ \t]+ – separator before verb
29
+ * (commit|merge|…) – captured verb
30
+ * \b – word boundary after verb
31
+ */
32
+ const GIT_TOKEN = /[^\s"']+/;
33
+ const GIT_VERB_RE = new RegExp(
34
+ "(?<!['\"])" +
35
+ "\\bgit\\b" +
36
+ "(?:[ \\t]+(?:" +
37
+ "(?:-C|-c|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path)" +
38
+ "(?:=" + GIT_TOKEN.source + "|[ \\t]+" + GIT_TOKEN.source + ")?" +
39
+ "|-{1,2}[A-Za-z][\\w-]*(?:=" + GIT_TOKEN.source + ")?" +
40
+ "))*" +
41
+ "[ \\t]+" +
42
+ "(commit|merge|rebase|cherry-pick|revert|am|pull|(?<!stash )push)\\b"
43
+ );
44
+
45
+ /**
46
+ * SHELL_C_RE equivalent — extracts payloads from `sh -c "..."` / `bash -c '...'`
47
+ * so that GIT_VERB_RE can check them (quoted payloads would otherwise be
48
+ * invisible due to the (?<!['"]) anchor).
49
+ *
50
+ * Python source: hooks/guards.py lines 139-140
51
+ */
52
+ const SHELL_C_RE = /\b(?:sh|bash|zsh|dash|ksh)\b[^|;&\n\r]*?-c[ \t]+(?:"([^"]*)"|'([^']*)')/;
53
+
54
+ /**
55
+ * AUTHORITY_RE equivalent — scoped to `ai/<run>/` prefix and matching ALL
56
+ * integrity-critical filenames. False-positive-safe because it requires the
57
+ * `ai/` prefix (matches Python's behavior exactly).
58
+ *
59
+ * Python source: hooks/guards.py lines 149-152
60
+ */
61
+ const AUTHORITY_RE = /ai[/\\][^/\\\s"']+[/\\](?:state\.yaml|events\.ndjson|tokens\.ndjson|human-input\.ndjson|reviews\.ndjson|\.redproof|\.state\.lock)\b|\.hmac\b/;
62
+
63
+ /**
64
+ * WRITE_HINT_RE equivalent — detects inline programming-language writes that
65
+ * circumvent path-based write blocking (shell redirects, tee, sed, python
66
+ * open/write, node fs.writeFile, etc.).
67
+ *
68
+ * Python source: hooks/guards.py lines 162-173
69
+ */
70
+ const WRITE_HINT_RE = /(?<![0-9])>(?!&)|\btee\b|\bsed\s+(-\w+\s+)*-i|\brm\b|\bmv\b|\bcp\b|\btruncate\b|\bdd\b|yq\s+.*-i|--in-place/;
71
+
72
+ /**
73
+ * Agent shapes in the harness — matches Python's shape_of().
74
+ * Python source: hooks/guards.py lines 499-502
75
+ */
76
+ const HARNESS_SHAPES = ["planner", "developer", "reviewer"];
77
+
78
+ // ── Helpers ──────────────────────────────────────────────────────────────
79
+
80
+ /** Normalize agent name the same way Python's shape_of() does. */
81
+ function normalizeAgent(raw: string): string {
82
+ const tail = raw.split(":").pop() ?? "";
83
+ return tail.trim().toLowerCase().replace(/^ai-sdlc-/, "");
84
+ }
85
+
86
+ /** Recursively extract all shell command targets from a bash command line. */
87
+ function extractTargets(cmd: string): string[] {
88
+ const targets = [cmd];
89
+ // Extract sh -c / bash -c payloads
90
+ const m = SHELL_C_RE.exec(cmd);
91
+ if (m) {
92
+ const payload = m[1] ?? m[2] ?? "";
93
+ if (payload) targets.push(...extractTargets(payload));
94
+ }
95
+ return targets;
96
+ }
97
+
98
+ /** Check if ANY target contains a blocked git verb. */
99
+ function hasBlockedGitVerb(cmd: string): boolean {
100
+ for (const target of extractTargets(cmd)) {
101
+ if (GIT_VERB_RE.test(target)) return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /** Check if a bash command contains inline/injected write operations. */
107
+ function hasWriteHint(cmd: string): boolean {
108
+ return WRITE_HINT_RE.test(cmd);
109
+ }
110
+
111
+ // ── Plugin ───────────────────────────────────────────────────────────────
112
+
113
+ const HarnessGuardsPlugin: Plugin = async ({ project, $, directory }) => {
114
+ const rawName =
115
+ (project as { agent?: { name?: string } }).agent?.name ?? "";
116
+ const agentName = normalizeAgent(rawName) || "direct-user";
117
+ const guardsPy = `${directory}/hooks/guards.py`;
118
+ const encoder = new TextEncoder();
119
+
120
+ // ── Layer 1: Enforcement via permission.ask ──────────────────────────
121
+ async function handlePermissionAsk(
122
+ permission: PermissionEvent,
123
+ output: { status: "ask" | "deny" | "allow" },
124
+ ): Promise<void> {
125
+ try {
126
+ if (output.status === "deny") return; // Already denied upstream
127
+
128
+ switch (permission.type) {
129
+ case "bash": {
130
+ const cmd = permission.command ?? "";
131
+ if (!cmd) break;
132
+
133
+ // Block git verbs (matching Python guard_bash)
134
+ if (hasBlockedGitVerb(cmd)) {
135
+ output.status = "deny";
136
+ console.warn(
137
+ `[harness-guards] DENIED bash (git verb): ${cmd.slice(0, 120)}`,
138
+ );
139
+ break;
140
+ }
141
+
142
+ // Block inline writes to authority paths via shell commands
143
+ if (hasWriteHint(cmd)) {
144
+ // For now, only block if a write hint targets an authority path.
145
+ // Full authority-path write detection requires command parsing
146
+ // (the Python guard uses both WRITE_HINT_RE + AUTHORITY_RE).
147
+ // This is a best-effort check; the Python audit (Layer 2) catches
148
+ // the rest.
149
+ output.status = "deny";
150
+ console.warn(
151
+ `[harness-guards] DENIED bash (write hint): ${cmd.slice(0, 120)}`,
152
+ );
153
+ break;
154
+ }
155
+
156
+ // Not a dangerous command — auto-allow to avoid prompt fatigue.
157
+ // Without this, every safe command would prompt the user.
158
+ output.status = "allow";
159
+ break;
160
+ }
161
+
162
+ case "write":
163
+ case "edit": {
164
+ const filePath = permission.path ?? "";
165
+ if (!filePath) break;
166
+
167
+ // Block writes to authority files (Python guard_write: AUTHORITY_RE)
168
+ if (AUTHORITY_RE.test(filePath)) {
169
+ output.status = "deny";
170
+ console.warn(
171
+ `[harness-guards] DENIED ${permission.type} (authority path): ${filePath}`,
172
+ );
173
+ break;
174
+ }
175
+
176
+ // Auto-allow to avoid prompt fatigue for non-authority paths.
177
+ // The permission patterns in opencode.jsonc still deny writes
178
+ // outside allowed directories.
179
+ output.status = "allow";
180
+ break;
181
+ }
182
+
183
+ case "read": {
184
+ const filePath = permission.path ?? "";
185
+ if (!filePath) break;
186
+
187
+ // Block raw .redproof reads for ALL harness shapes
188
+ // (Python guard_read blocks for planner, developer, reviewer)
189
+ if (
190
+ HARNESS_SHAPES.includes(agentName) &&
191
+ /\.redproof[/\\]/.test(filePath)
192
+ ) {
193
+ output.status = "deny";
194
+ console.warn(
195
+ `[harness-guards] DENIED read (redproof isolation): ${filePath}`,
196
+ );
197
+ }
198
+ // Leave non-redproof reads at their configured status
199
+ break;
200
+ }
201
+ }
202
+ } catch (err) {
203
+ // Error boundary — never let a plugin crash allow a blocked action
204
+ console.error(`[harness-guards] ERROR in permission.ask: ${err}`);
205
+ }
206
+ }
207
+
208
+ // ── Layer 2: Audit via Python guards (non-blocking) ─────────────────
209
+ async function auditGuard(
210
+ guard: string,
211
+ payload: Record<string, unknown>,
212
+ ): Promise<void> {
213
+ const t0 = Date.now();
214
+ try {
215
+ const proc = $`python3 ${guardsPy} ${guard}`.quiet().nothrow();
216
+ const writer = proc.stdin.getWriter();
217
+ await writer.write(encoder.encode(JSON.stringify(payload)));
218
+ await writer.close();
219
+
220
+ // 5-second timeout to prevent a hung Python process from stalling tools
221
+ const output = await Promise.race([
222
+ proc,
223
+ new Promise<never>((_, reject) =>
224
+ setTimeout(() => reject(new Error("timeout")), 5000),
225
+ ),
226
+ ]);
227
+
228
+ const elapsed = Date.now() - t0;
229
+ if (output.exitCode === 2) {
230
+ const reason = output.stderr?.toString()?.trim() ?? "unknown";
231
+ console.warn(
232
+ `[harness-guards] ${guard} BLOCK (${elapsed}ms): ${reason}`,
233
+ );
234
+ } else if (output.exitCode !== 0) {
235
+ console.warn(
236
+ `[harness-guards] ${guard} exit ${output.exitCode} (${elapsed}ms)`,
237
+ );
238
+ }
239
+ } catch (err) {
240
+ // Fail open — audit should never impede the tool
241
+ const elapsed = Date.now() - t0;
242
+ if (
243
+ err instanceof Error &&
244
+ err.message === "timeout" &&
245
+ elapsed >= 5000
246
+ ) {
247
+ console.warn(
248
+ `[harness-guards] ${guard} TIMEOUT after ${elapsed}ms`,
249
+ );
250
+ } else {
251
+ console.warn(`[harness-guards] ${guard} ERROR: ${err}`);
252
+ }
253
+ }
254
+ }
255
+
256
+ return {
257
+ // Layer 1: Enforcement
258
+ "permission.ask": handlePermissionAsk,
259
+
260
+ // Layer 2: Audit
261
+ "tool.execute.before": async (_input, output) => {
262
+ const tool = (_input as { tool?: string }).tool ?? "unknown";
263
+ const args = output?.args ?? {};
264
+ const payload: Record<string, unknown> = {
265
+ agent_type: agentName,
266
+ cwd: directory,
267
+ tool_input: args,
268
+ };
269
+
270
+ switch (tool) {
271
+ case "bash":
272
+ await auditGuard("bash", payload);
273
+ break;
274
+ case "write":
275
+ case "edit":
276
+ await auditGuard("write", payload);
277
+ break;
278
+ case "read":
279
+ case "grep":
280
+ await auditGuard("read", payload);
281
+ break;
282
+ case "task":
283
+ await auditGuard("spawn", payload);
284
+ break;
285
+ }
286
+ },
287
+ };
288
+ };
289
+
290
+ export default HarnessGuardsPlugin;