ultimate-pi 0.18.0 → 0.18.1

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 (68) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  2. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  3. package/.agents/skills/harness-review/SKILL.md +7 -7
  4. package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
  5. package/.agents/skills/harness-steer/SKILL.md +1 -1
  6. package/.agents/skills/sentrux/SKILL.md +9 -9
  7. package/.pi/agents/harness/planning/decompose.md +1 -1
  8. package/.pi/extensions/00-harness-project-control.ts +133 -0
  9. package/.pi/extensions/budget-guard.ts +2 -0
  10. package/.pi/extensions/debate-orchestrator.ts +2 -0
  11. package/.pi/extensions/harness-ask-user.ts +2 -2
  12. package/.pi/extensions/harness-debate-tools.ts +2 -2
  13. package/.pi/extensions/harness-live-widget.ts +33 -2
  14. package/.pi/extensions/harness-plan-approval.ts +2 -2
  15. package/.pi/extensions/harness-run-context.ts +180 -12
  16. package/.pi/extensions/harness-subagent-submit.ts +3 -2
  17. package/.pi/extensions/harness-subagents.ts +2 -2
  18. package/.pi/extensions/harness-telemetry.ts +2 -0
  19. package/.pi/extensions/harness-web-tools.ts +2 -2
  20. package/.pi/extensions/lib/extension-load-guard.ts +10 -0
  21. package/.pi/extensions/lib/harness-artifact-gate.ts +5 -15
  22. package/.pi/extensions/lib/harness-spawn-topology.ts +4 -27
  23. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -2
  24. package/.pi/extensions/lib/harness-subagent-policy.ts +5 -5
  25. package/.pi/extensions/lib/harness-subagent-precheck.ts +3 -3
  26. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +3 -21
  27. package/.pi/extensions/lib/plan-approval-readiness.ts +3 -52
  28. package/.pi/extensions/lib/spawn-policy.ts +3 -3
  29. package/.pi/extensions/observation-bus.ts +2 -0
  30. package/.pi/extensions/policy-gate.ts +2 -0
  31. package/.pi/extensions/review-integrity.ts +91 -10
  32. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  33. package/.pi/extensions/test-diff-integrity.ts +1 -0
  34. package/.pi/extensions/trace-recorder.ts +2 -0
  35. package/.pi/harness/agents.manifest.json +23 -31
  36. package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
  37. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
  38. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
  39. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  40. package/.pi/harness/docs/adrs/README.md +1 -0
  41. package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
  42. package/.pi/harness/docs/practice-map.md +2 -2
  43. package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
  44. package/.pi/lib/harness-project-config.ts +91 -0
  45. package/.pi/lib/harness-run-context.ts +1 -1
  46. package/.pi/lib/harness-ui-state.ts +27 -12
  47. package/.pi/prompts/harness-auto.md +2 -2
  48. package/.pi/prompts/harness-critic.md +1 -1
  49. package/.pi/prompts/harness-plan.md +3 -5
  50. package/.pi/prompts/harness-review.md +9 -9
  51. package/.pi/prompts/harness-run.md +7 -7
  52. package/.pi/prompts/harness-setup.md +5 -4
  53. package/.pi/prompts/harness-steer.md +2 -2
  54. package/.pi/scripts/README.md +1 -0
  55. package/.pi/scripts/graphify-kb-updater.mjs +48 -8
  56. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  57. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  58. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  59. package/CHANGELOG.md +12 -0
  60. package/README.md +94 -58
  61. package/package.json +3 -3
  62. package/.pi/agents/harness/planning/scout-graphify.md +0 -39
  63. package/.pi/agents/harness/planning/scout-semantic.md +0 -41
  64. package/.pi/agents/harness/planning/scout-structure.md +0 -37
  65. /package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -0
  66. /package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -0
  67. /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
  68. /package/.pi/agents/harness/{executor.md → running/executor.md} +0 -0
@@ -375,8 +375,6 @@ export const HARNESS_PHASE_ORDER: readonly HarnessPhase[] = [
375
375
  "plan",
376
376
  "execute",
377
377
  "evaluate",
378
- "adversary",
379
- "merge",
380
378
  ] as const;
381
379
 
382
380
  export function formatHarnessPhaseLabel(phase: HarnessPhase): string {
@@ -384,13 +382,11 @@ export function formatHarnessPhaseLabel(phase: HarnessPhase): string {
384
382
  case "plan":
385
383
  return "plan";
386
384
  case "execute":
387
- return "build";
385
+ return "run";
388
386
  case "evaluate":
389
- return "eval";
390
387
  case "adversary":
391
- return "review";
392
388
  case "merge":
393
- return "merge";
389
+ return "review";
394
390
  }
395
391
  }
396
392
 
@@ -400,6 +396,25 @@ export function nextHarnessPhase(phase: HarnessPhase): HarnessPhase | null {
400
396
  return HARNESS_PHASE_ORDER[index + 1] ?? null;
401
397
  }
402
398
 
399
+ function mainPhaseCommandForStatus(state: HarnessUiState): string | null {
400
+ const command = state.nextRecommendedCommand;
401
+ if (!command) return null;
402
+ const normalized = command.toLowerCase();
403
+
404
+ if (normalized.includes("/harness-plan")) {
405
+ return normalized.includes("revise")
406
+ ? "/harness-plan (mode: revise)"
407
+ : "/harness-plan";
408
+ }
409
+ if (normalized.includes("/harness-review")) return "/harness-review";
410
+ if (normalized.includes("/harness-run-status")) {
411
+ return state.phase === "execute" ? "/harness-review" : null;
412
+ }
413
+ if (normalized.includes("/harness-run")) return "/harness-run";
414
+ if (normalized.includes("/harness-steer")) return "/harness-run";
415
+ return null;
416
+ }
417
+
403
418
  function truncateStatusCommand(command: string, maxLen = 40): string {
404
419
  if (command.length <= maxLen) return command;
405
420
  return `${command.slice(0, maxLen - 3)}...`;
@@ -430,9 +445,10 @@ export function deriveHarnessStatusHint(state: HarnessUiState): {
430
445
  ) {
431
446
  return { text: "Waiting for your input", severity: "warning" };
432
447
  }
433
- if (state.nextRecommendedCommand) {
448
+ const mainPhaseCommand = mainPhaseCommandForStatus(state);
449
+ if (mainPhaseCommand) {
434
450
  return {
435
- text: `Next: ${truncateStatusCommand(state.nextRecommendedCommand)}`,
451
+ text: `Next: ${truncateStatusCommand(mainPhaseCommand)}`,
436
452
  severity: "accent",
437
453
  };
438
454
  }
@@ -450,13 +466,12 @@ export function deriveHarnessStatusHint(state: HarnessUiState): {
450
466
  }
451
467
  switch (state.phase) {
452
468
  case "execute":
453
- return { text: "Implementing changes", severity: "accent" };
469
+ return { text: "Running changes", severity: "accent" };
454
470
  case "evaluate":
455
- return { text: "Running checks", severity: "accent" };
456
471
  case "adversary":
457
- return { text: "Review gate", severity: "accent" };
472
+ return { text: "Reviewing changes", severity: "accent" };
458
473
  case "merge":
459
- return { text: "Ready to finish", severity: "accent" };
474
+ return { text: "Review complete", severity: "accent" };
460
475
  default:
461
476
  return { text: "Planning", severity: "muted" };
462
477
  }
@@ -21,7 +21,7 @@ If task missing:
21
21
  Follow **harness-plan** performance rules (`subagent` with `agentScope: "both"`). Use parallel `tasks` only for Phase 3.5 research (≤2 lanes) when subprocesses are needed. Never parallelize decompose∥hypothesis or debate lanes — precheck enforces this.
22
22
 
23
23
  1. **Plan** — follow `/harness-plan` (context → lakes/synthesis or sequential framing → research → plan-verify → `approve_plan()` + `create_plan()`). One approval.
24
- 2. **Execute** — `harness/executor` with `executor_strategy` from packet (default `single_pass` for low/med).
24
+ 2. **Execute** — `harness/running/executor` with `executor_strategy` from packet (default `single_pass` for low/med).
25
25
  3. **Review** — always **`/harness-review`** after execute (no benchmark fail-fast).
26
26
  4. **Steer loop** — while `review-outcome.remediation_class === implementation_gap` and `steer_attempt < HARNESS_STEER_MAX_ATTEMPTS`: `/harness-steer` → `/harness-review` (tiered adversary on attempts 2+).
27
27
  5. **Parent** — apply locked strict gates; commit/PR only when `remediation_class: pass`.
@@ -50,7 +50,7 @@ Block commit/PR if any fails: plan gate, execution in scope, evaluator pass, adv
50
50
  - `--quick` reduces breadth (skips semantic coverage in planning context, post-run adversary, tie-breaker), never core safety gates on plan approval or evaluator.
51
51
  - High risk/ambiguity → stop and recommend manual `/harness-plan` with `ask_user`.
52
52
  - Interrupt: `/harness-abort [reason]` then `/harness-plan`.
53
- - Artifact refs under active run dir; `/harness-run-status` or `/harness-trace-last` for handoff.
53
+ - Artifact refs under active run dir; use `/harness-trace` for handoff and forensics.
54
54
 
55
55
  ## Completion
56
56
 
@@ -5,6 +5,6 @@ argument-hint: "[--run <run-id>] [--trace <trace-ref>] [--risk low|med|high]"
5
5
 
6
6
  # harness-critic
7
7
 
8
- **This command is deprecated.** Run **`/harness-review`** instead — Phase 4 runs `harness/adversary` after benchmark and policy verdict pass (skip with `--quick`).
8
+ **This command is deprecated.** Run **`/harness-review`** instead — Phase 4 runs `harness/reviewing/adversary` after benchmark and policy verdict pass (skip with `--quick`).
9
9
 
10
10
  If you must continue this turn only: forward to `/harness-review` with the same `$ARGUMENTS` (omit `--quick` if you need adversary). Do not spawn adversary in isolation unless the user explicitly requested adversary-only review.
@@ -26,8 +26,6 @@ Subagents persist artifacts via scoped **`submit_*`** tools (deterministic YAML
26
26
  - `harness/planning/sprint-contract-auditor` (DoD auditor)
27
27
  - `harness/planning/review-integrator` (recorder / integration PM)
28
28
 
29
- Legacy (deprecated, ADR 0041): `scout-graphify`, `scout-structure`, `scout-semantic` — do not spawn by default.
30
-
31
29
  Read **harness-debate-plan** skill before Review Gate rounds.
32
30
 
33
31
  ## Team topology (spawn laws)
@@ -72,16 +70,16 @@ Do **not** run `ccc index` or `ccc search --refresh`. The harness runs increment
72
70
  3. Use `ccc search` for semantic implementation matches (unless `--quick` — set `coverage.semantic.status: skipped`).
73
71
  4. Write `artifacts/planning-context.yaml` via `write_harness_yaml` with `schema_version: "1.0.0"`, `status`, `summary`, `coverage` (architecture + structure required; semantic per risk/quick), `findings`, `evidence_refs`, `open_questions`.
74
72
 
75
- **Optional subprocess:** Spawn **at most one** `harness/planning/planning-context` when the brief is large or you need context isolation. Do **not** spawn legacy `scout-*` agents in parallel by default.
73
+ **Optional subprocess:** Spawn **at most one** `harness/planning/planning-context` when the brief is large or you need context isolation.
76
74
 
77
- Gate: `harness_artifact_ready({ paths: ["artifacts/planning-context.yaml"] })` (legacy trio of `scout-*.yaml` still accepted for one release — see ADR 0041).
75
+ Gate: `harness_artifact_ready({ paths: ["artifacts/planning-context.yaml"] })`.
78
76
 
79
77
  ## Phase 2a — WBS / scope decomposition (sequential)
80
78
 
81
79
  **Practice:** PMBOK scope / WBS; Berkun — how the team divides work.
82
80
 
83
81
  ```
84
- subagent({ agentScope: "both", agent: "harness/planning/decompose", task: "<HarnessSpawnContext + path to planning-context.yaml or legacy scout artifacts>" })
82
+ subagent({ agentScope: "both", agent: "harness/planning/decompose", task: "<HarnessSpawnContext + path to planning-context.yaml>" })
85
83
  ```
86
84
 
87
85
  Gate: `harness_artifact_ready({ paths: ["artifacts/decomposition.yaml"] })`.
@@ -13,9 +13,9 @@ Read **harness-orchestration** and **harness-review** skills before spawning.
13
13
 
14
14
  ## Allowed subagents
15
15
 
16
- - `harness/evaluator` (`mode: benchmark` then `mode: verdict`)
17
- - `harness/adversary` (independent red team)
18
- - `harness/tie-breaker` (escalation only when adversary blocks and eval was `conditional_pass`; skip when `--quick`)
16
+ - `harness/reviewing/evaluator` (`mode: benchmark` then `mode: verdict`)
17
+ - `harness/reviewing/adversary` (independent red team)
18
+ - `harness/reviewing/tie-breaker` (escalation only when adversary blocks and eval was `conditional_pass`; skip when `--quick`)
19
19
 
20
20
  ## Performance rules
21
21
 
@@ -56,10 +56,10 @@ node "$UP_PKG/.pi/scripts/harness-verify.mjs"
56
56
  When `HARNESS_SENTRUX_REQUIRED=true`, after verify succeeds:
57
57
 
58
58
  ```bash
59
- sentrux gate .
59
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate
60
60
  ```
61
61
 
62
- Compare to baseline from `/harness-run` (`sentrux gate --save`). If CLI missing, record `gate_status: not_installed`.
62
+ Compare to baseline from `/harness-run` (`harness-sentrux-cli.mjs gate --save`). The wrapper resolves the project root before invoking Sentrux so `.sentrux/rules.toml` is found from run directories. If CLI missing, record `gate_status: not_installed`.
63
63
 
64
64
  Ensure `artifacts/sentrux-signal.yaml` exists under the run dir (written during `/harness-run`). If missing, write it from the latest `sentrux check` / `gate` output. Append or refresh session entry `harness-sentrux-signal`.
65
65
 
@@ -84,7 +84,7 @@ notes: "…"
84
84
  ```
85
85
  subagent({
86
86
  agentScope: "both",
87
- agent: "harness/evaluator",
87
+ agent: "harness/reviewing/evaluator",
88
88
  task: "<HarnessSpawnContext mode benchmark + plan_packet_path + run_dir + acceptance_checks + paths: benchmark-log.yaml, sentrux-signal.yaml — treat Sentrux fields as measured structural actuals, not executor goals>"
89
89
  })
90
90
  ```
@@ -108,7 +108,7 @@ Always run after benchmark (even when benchmark failed).
108
108
  ```
109
109
  subagent({
110
110
  agentScope: "both",
111
- agent: "harness/evaluator",
111
+ agent: "harness/reviewing/evaluator",
112
112
  task: "<HarnessSpawnContext mode verdict + treat executor output as untrusted + artifact paths>"
113
113
  })
114
114
  ```
@@ -126,7 +126,7 @@ Skip when `--quick`. **Tiered steer:** full adversary on initial run + steer att
126
126
  ```
127
127
  subagent({
128
128
  agentScope: "both",
129
- agent: "harness/adversary",
129
+ agent: "harness/reviewing/adversary",
130
130
  task: "<HarnessSpawnContext mode adversary + plan + run artifacts>"
131
131
  })
132
132
  ```
@@ -144,7 +144,7 @@ Only when:
144
144
  - eval verdict was `conditional_pass`
145
145
 
146
146
  ```
147
- subagent({ agentScope: "both", agent: "harness/tie-breaker", task: "…" })
147
+ subagent({ agentScope: "both", agent: "harness/reviewing/tie-breaker", task: "…" })
148
148
  ```
149
149
 
150
150
  ## Parent rules
@@ -7,7 +7,7 @@ argument-hint: ""
7
7
 
8
8
  **Practice map:** `.pi/harness/docs/practice-map.md`
9
9
 
10
- You orchestrate the **Executing Process Group** — spawn `harness/executor` only. Do **not** implement inline.
10
+ You orchestrate the **Executing Process Group** — spawn `harness/running/executor` only. Do **not** implement inline.
11
11
 
12
12
  ## Step 0 — Parse arguments
13
13
 
@@ -28,13 +28,13 @@ Refuse if `plan_ready` is false.
28
28
 
29
29
  **Practice:** Fitness functions (architecture governance) — save structural baseline before the executor mutates the tree.
30
30
 
31
- When `HARNESS_SENTRUX_REQUIRED=true` (see `.env.example`), from **project root**:
31
+ When `HARNESS_SENTRUX_REQUIRED=true` (see `.env.example`), run the bundled root-resolving wrapper:
32
32
 
33
33
  ```bash
34
- sentrux gate --save .
34
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate --save
35
35
  ```
36
36
 
37
- If `sentrux` is not installed, note `gate_baseline: skipped` in run notes and continue (harness-verify may still pass rules-sync checks).
37
+ The wrapper passes the resolved project root explicitly so Sentrux can find `.sentrux/rules.toml` even if the active shell is under `.pi/harness/runs/*`. If `sentrux` is not installed, note `gate_baseline: skipped` in run notes and continue (harness-verify may still pass rules-sync checks).
38
38
 
39
39
  Do **not** ask the executor to optimize Sentrux metrics — observation is for `/harness-review` only.
40
40
 
@@ -48,7 +48,7 @@ Do **not** ask the executor to optimize Sentrux metrics — observation is for `
48
48
  4. Spawn (max **1** agent per call):
49
49
 
50
50
  ```
51
- subagent({ agentScope: "both", agent: "harness/executor", task: "<HarnessSpawnContext + handoff + critical path hint>" })
51
+ subagent({ agentScope: "both", agent: "harness/running/executor", task: "<HarnessSpawnContext + handoff + critical path hint>" })
52
52
  ```
53
53
 
54
54
  5. Parse subprocess output JSON (`execution_status`, validations, rollback refs) from tool result text.
@@ -61,8 +61,8 @@ subagent({ agentScope: "both", agent: "harness/executor", task: "<HarnessSpawnCo
61
61
  After executor subprocess completes:
62
62
 
63
63
  ```bash
64
- sentrux check .
65
- sentrux gate .
64
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" check
65
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate
66
66
  ```
67
67
 
68
68
  - If `sentrux check` exits non-zero or `gate` reports degradation → set `execution_status: scope_drift` (or `blocked` if unrecoverable); parent runs **`/harness-review`** next (not immediate replan).
@@ -387,11 +387,11 @@ Manual override: **`/router profile auto`** or **`/router profile opencode-go`**
387
387
 
388
388
  ## Step 3.6 — Harness agents (package-resolved)
389
389
 
390
- `harness-subagents` loads agents from the installed **`ultimate-pi`** package (`$UP_PKG/.pi/agents/**`) with namespaced ids (`harness/executor`, `harness/planning/scout-graphify`, `pi-pi/agent-expert`). **Do not copy** agents into the project unless you want a deliberate override.
390
+ `harness-subagents` loads agents from the installed **`ultimate-pi`** package (`$UP_PKG/.pi/agents/**`) with namespaced ids (`harness/running/executor`, `harness/reviewing/evaluator`, `pi-pi/agent-expert`). **Do not copy** agents into the project unless you want a deliberate override.
391
391
 
392
392
  **Slash commands are orchestrators:** `/harness-plan`, `/harness-run`, etc. spawn `harness/*` agents via the `Agent` tool — bootstrap stays **script-first**; only optionally spawn `harness/sentrux-bootstrap` for Sentrux (see Step 4.2).
393
393
 
394
- Optional per-repo overrides: place `.md` files at the **same relative path** (e.g. `.pi/agents/harness/planning/scout-graphify.md` overrides the package scout).
394
+ Optional per-repo overrides: place `.md` files at the **same relative path** (e.g. `.pi/agents/harness/running/executor.md` overrides the package executor).
395
395
 
396
396
  Verify manifest drift after `pi update ultimate-pi`:
397
397
 
@@ -531,18 +531,19 @@ node "$UP_PKG/.pi/scripts/harness-sentrux-bootstrap.mjs" --force
531
531
  | `harness-sentrux-bootstrap.mjs` (no flags) | `/harness-setup`, first install, re-run safe |
532
532
  | `harness-sentrux-bootstrap.mjs --force` | Manifest layers/boundaries/constraints changed |
533
533
  | `sentrux-rules-sync.mjs --check` | CI / harness-verify drift only |
534
+ | `harness-sentrux-cli.mjs check` / `gate` | Root-resolving Sentrux checks from harness run dirs |
534
535
  | `/harness-sentrux-sync` | Interactive re-sync from pi |
535
536
 
536
537
  `harness-seed-project-contracts.mjs` (Step 0.5) may copy `architecture.manifest.json` early; bootstrap still personalizes `project` on first seed and writes `rules.toml`.
537
538
 
538
539
  Verify rules:
539
540
  ```bash
540
- sentrux check . && echo "✓ sentrux rules pass" || echo "✗ sentrux check failed"
541
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" check && echo "✓ sentrux rules pass" || echo "✗ sentrux check failed"
541
542
  ```
542
543
 
543
544
  Set up structural regression baseline (optional):
544
545
  ```bash
545
- sentrux gate --save . 2>/dev/null || echo "Baseline will be saved on first gate run"
546
+ node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate --save 2>/dev/null || echo "Baseline will be saved on first gate run"
546
547
  ```
547
548
 
548
549
  ### 4.3 — Project AGENTS.md
@@ -19,8 +19,8 @@ Thin orchestrator for the **steer loop** (ADR 0044). Run only after `/harness-re
19
19
  2. Update `artifacts/steer-state.yaml` (`attempt`, `max_attempts`, `active: true`).
20
20
  3. Set policy phase to **execute** before spawning executor (required for mutating tools).
21
21
  4. One `ask_user` steer gate unless `run-context.steer_approved` is already true.
22
- 5. Spawn **`harness/executor`** with `HarnessSpawnContext.mode: repair` and `repair_brief_path: artifacts/repair-brief.yaml`.
23
- 6. Optional: `sentrux gate --save .` after repair to refresh baseline (ADR 0044).
22
+ 5. Spawn **`harness/running/executor`** with `HarnessSpawnContext.mode: repair` and `repair_brief_path: artifacts/repair-brief.yaml`.
23
+ 6. Optional: `node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" gate --save` after repair to refresh baseline (ADR 0044).
24
24
  7. `next_command`: **`/harness-review`** (always re-verify; tiered adversary on attempts 2+ per practice-map).
25
25
 
26
26
  ## Forbidden
@@ -27,6 +27,7 @@ From **Typescript extensions**, use `resolveHarnessScript()` / `getHarnessPackag
27
27
  | Sentrux rules bootstrap (harness-setup) | `node "$UP_PKG/.pi/scripts/harness-sentrux-bootstrap.mjs"` |
28
28
  | Sentrux rules re-sync after manifest edit | `node "$UP_PKG/.pi/scripts/harness-sentrux-bootstrap.mjs" --force` or `/harness-sentrux-sync` |
29
29
  | Sentrux rules drift check (CI) | `node "$UP_PKG/.pi/scripts/sentrux-rules-sync.mjs" --check` |
30
+ | Sentrux run/review check or gate (root-resolving) | `node "$UP_PKG/.pi/scripts/harness-sentrux-cli.mjs" check` / `gate [--save]` |
30
31
  | Resolve package root (`UP_PKG`) | `node "$UP_PKG/.pi/scripts/harness-resolve-up-pkg.mjs"` |
31
32
  | Model-router config (Pi auth) | `node "$UP_PKG/.pi/scripts/harness-generate-model-router.mjs"` |
32
33
  | Project `.env` (append-only) | `node "$UP_PKG/.pi/scripts/harness-sync-env.mjs"` (`--create-missing` after user confirms) |
@@ -20,6 +20,7 @@ const DEFAULT_RAW_DIR = "raw/graphify-kb-updates";
20
20
  const DEFAULT_DATA_DIR = "data";
21
21
  const DEFAULT_GRAPH_DIR = "graphify-out";
22
22
  const REQUIRED_RIGHTS = ["license", "access", "approved_by", "approved_at"];
23
+ const REQUIRED_PROVENANCE = ["origin", "locator"];
23
24
  const RISKY_KINDS = new Set(["book", "transcript", "youtube"]);
24
25
 
25
26
  function parseArgs(argv) {
@@ -92,6 +93,8 @@ function loadConfig(args) {
92
93
  allowlist: (cfg.allowlist ?? []).map((entry) => typeof entry === "string" ? { domain: entry, approved: true } : entry),
93
94
  reviewQueue: cfg.review_queue ?? [],
94
95
  articleQueries: cfg.article_queries ?? [],
96
+ repoSources: cfg.repo_sources ?? [],
97
+ releaseFeeds: cfg.release_feeds ?? [],
95
98
  paperFeeds: cfg.paper_feeds ?? [],
96
99
  localBooks: cfg.local_books ?? [{ path: "data/books" }],
97
100
  localTranscripts: cfg.local_transcripts ?? [{ path: "data/youtube-transcripts" }],
@@ -130,6 +133,11 @@ function hasRightsApproval(candidate) {
130
133
  return Boolean(r && REQUIRED_RIGHTS.every((k) => typeof r[k] === "string" && r[k].trim()));
131
134
  }
132
135
 
136
+ function hasCompleteProvenance(candidate) {
137
+ const p = candidate.provenance;
138
+ return Boolean(p && REQUIRED_PROVENANCE.every((k) => typeof p[k] === "string" && p[k].trim()));
139
+ }
140
+
133
141
  function urlDomain(url) {
134
142
  try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }
135
143
  }
@@ -138,6 +146,14 @@ function allowlistEntry(cfg, domain) {
138
146
  return cfg.allowlist.find((entry) => entry.domain === domain || domain.endsWith(`.${entry.domain}`));
139
147
  }
140
148
 
149
+ function allowlistEntryAllows(entry, kind) {
150
+ return Boolean(entry?.approved === true && (!Array.isArray(entry.allowed_source_classes) || entry.allowed_source_classes.includes(kind)));
151
+ }
152
+
153
+ function allowlistAllows(candidate) {
154
+ return Boolean(candidate.allowlist_state.allowed && candidate.allowlist_state.approved && (!Array.isArray(candidate.allowlist_state.allowed_source_classes) || candidate.allowlist_state.allowed_source_classes.includes(candidate.kind)));
155
+ }
156
+
141
157
  function competitorLabels(cfg, candidate) {
142
158
  const haystack = `${candidate.title ?? ""} ${candidate.url ?? ""} ${candidate.path ?? ""}`.toLowerCase();
143
159
  const labels = [];
@@ -163,7 +179,7 @@ function normalizeCandidate(cfg, raw) {
163
179
  risk_class: raw.risk_class ?? taxonomy.risk_class ?? (RISKY_KINDS.has(raw.kind) ? "high" : "medium"),
164
180
  provenance: raw.provenance ?? { origin: raw.source_type, discovered_by: "graphify-kb-updater", locator: raw.url ?? raw.path ?? raw.query ?? null },
165
181
  rights_access: raw.rights_access ?? null,
166
- allowlist_state: allow ? { allowed: true, domain: allow.domain, approved: allow.approved === true, approved_by: allow.approved_by ?? null, approved_at: allow.approved_at ?? null } : { allowed: false },
182
+ allowlist_state: allow ? { allowed: true, domain: allow.domain, approved: allow.approved === true, approved_by: allow.approved_by ?? null, approved_at: allow.approved_at ?? null, allowed_source_classes: allow.allowed_source_classes ?? null } : { allowed: false },
167
183
  approval_state: raw.approved === true ? "approved" : "not_approved",
168
184
  };
169
185
  candidate.competitor_labels = raw.competitor_labels ?? competitorLabels(cfg, candidate);
@@ -175,12 +191,27 @@ function normalizeCandidate(cfg, raw) {
175
191
  function discoverCandidates(cfg, args) {
176
192
  const candidates = [];
177
193
  for (const query of cfg.articleQueries) candidates.push({ kind: "article", source_type: "web_search_query", title: query, query, review_required: true, promotion_policy: "stage_only" });
194
+ for (const repo of cfg.repoSources) {
195
+ const kind = "repo";
196
+ const domain = urlDomain(repo.url);
197
+ const allow = allowlistEntry(cfg, domain);
198
+ const explicit = cfg.autoPromoteAllowlist && allowlistEntryAllows(allow, kind) && repo.approved === true;
199
+ candidates.push({ ...repo, kind, source_type: "repo_metadata", domain, review_required: !explicit, promotion_policy: explicit ? "allowlist_auto_promote" : "manual_review", rights_access: repo.rights_access ?? null, provenance: repo.provenance });
200
+ }
201
+ for (const release of cfg.releaseFeeds) {
202
+ const kind = "release";
203
+ const domain = urlDomain(release.url);
204
+ const allow = allowlistEntry(cfg, domain);
205
+ const explicit = cfg.autoPromoteAllowlist && allowlistEntryAllows(allow, kind) && release.approved === true;
206
+ candidates.push({ ...release, kind, source_type: "release_metadata", domain, review_required: !explicit, promotion_policy: explicit ? "allowlist_auto_promote" : "manual_review", rights_access: release.rights_access ?? null, provenance: release.provenance });
207
+ }
178
208
  for (const feed of cfg.paperFeeds) candidates.push({ kind: "paper", source_type: "feed", title: feed.title ?? feed.url, url: feed.url, rights_access: feed.rights_access ?? null, review_required: true, promotion_policy: "stage_only", provenance: feed.provenance });
179
209
  for (const entry of cfg.reviewQueue) {
210
+ const kind = entry.kind ?? "article";
180
211
  const domain = urlDomain(entry.url);
181
212
  const allow = allowlistEntry(cfg, domain);
182
- const explicit = cfg.autoPromoteAllowlist && allow?.approved === true && entry.approved === true;
183
- candidates.push({ ...entry, kind: entry.kind ?? "article", source_type: "review_queue", domain, review_required: !explicit, promotion_policy: explicit ? "allowlist_auto_promote" : "manual_review", rights_access: entry.rights_access ?? null });
213
+ const explicit = cfg.autoPromoteAllowlist && allowlistEntryAllows(allow, kind) && entry.approved === true;
214
+ candidates.push({ ...entry, kind, source_type: "review_queue", domain, review_required: !explicit, promotion_policy: explicit ? "allowlist_auto_promote" : "manual_review", rights_access: entry.rights_access ?? null });
184
215
  }
185
216
  for (const spec of cfg.localBooks) for (const file of walkFiles(resolve(ROOT, spec.path), [".md", ".txt", ".pdf"], spec.max_files ?? 50)) candidates.push({ kind: "book", source_type: "local_file", title: basename(file), path: rel(file), rights_access: rightsFromSidecar(file), review_required: true, promotion_policy: "manual_review" });
186
217
  for (const spec of cfg.localTranscripts) for (const file of walkFiles(resolve(ROOT, spec.path), [".md", ".txt", ".vtt"], spec.max_files ?? 80)) candidates.push({ kind: "transcript", source_type: "local_file", title: basename(file), path: rel(file), rights_access: rightsFromSidecar(file), review_required: true, promotion_policy: "manual_review" });
@@ -208,10 +239,11 @@ function sourceBody(candidate) {
208
239
  }
209
240
 
210
241
  function promotionAllowed(candidate) {
242
+ if (!hasCompleteProvenance(candidate)) return { ok: false, reason: "missing_complete_provenance" };
211
243
  if (!hasRightsApproval(candidate)) return { ok: false, reason: "missing_rights_access_approval" };
212
244
  if (RISKY_KINDS.has(candidate.kind) && candidate.approved !== true) return { ok: false, reason: "manual_approval_required" };
213
- if (candidate.source_type === "review_queue" && candidate.promotion_policy === "allowlist_auto_promote" && candidate.allowlist_state.allowed && candidate.allowlist_state.approved) return { ok: true };
214
- return candidate.approved === true ? { ok: true } : { ok: false, reason: "manual_approval_required" };
245
+ if (candidate.promotion_policy === "allowlist_auto_promote" && candidate.approved === true && allowlistAllows(candidate)) return { ok: true, reason: "allowlist_auto_promote" };
246
+ return candidate.approved === true ? { ok: true, reason: "manual_approved" } : { ok: false, reason: "manual_approval_required" };
215
247
  }
216
248
 
217
249
  function promote(candidate, args) {
@@ -267,15 +299,19 @@ function pilotMetrics(summary) {
267
299
  function schedulerSmoke() {
268
300
  const service = readFileSync(resolve(ROOT, ".pi/harness/corpus/systemd/graphify-kb-updater.service"), "utf8");
269
301
  const timer = readFileSync(resolve(ROOT, ".pi/harness/corpus/systemd/graphify-kb-updater.timer"), "utf8");
302
+ const envTemplate = readFileSync(resolve(ROOT, ".pi/harness/corpus/systemd/graphify-kb-updater.env.template"), "utf8");
270
303
  const cron = readFileSync(resolve(ROOT, ".pi/harness/corpus/cron.example"), "utf8");
304
+ const graphifyArgs = envTemplate.match(/^GRAPHIFY_KB_ARGS=(.+)$/m)?.[1] ?? "";
271
305
  const checks = {
272
306
  systemd_daily: /OnCalendar=\*-\*-\*\s+08:30:00|OnCalendar=daily/i.test(timer),
273
307
  cron_daily: /^30\s+8\s+\*\s+\*\s+\*/m.test(cron),
274
308
  bounded_timeout: /timeout 45m/.test(service) && /timeout 45m/.test(cron),
275
309
  locked_no_overlap: /flock -n/.test(service) && /flock -n/.test(cron),
276
310
  explicit_env: /EnvironmentFile/.test(service) && /UP_ROOT/.test(cron),
311
+ working_directory: /WorkingDirectory=\$\{UP_ROOT\}/.test(service),
277
312
  logged: /StandardOutput=append/.test(service) && /HARNESS_GRAPHIFY_KB_LOG/.test(cron),
278
- refresh_intent: /--refresh-graph/.test(cron),
313
+ refresh_intent: /--refresh-graph/.test(cron) && /--refresh-graph/.test(graphifyArgs),
314
+ graphify_kb_args_template: /--apply/.test(graphifyArgs) && /--pilot-report/.test(graphifyArgs) && !/[;&|`$<>]/.test(graphifyArgs),
279
315
  };
280
316
  const ok = Object.values(checks).every(Boolean);
281
317
  console.log(JSON.stringify({ ok, checks }, null, 2));
@@ -305,8 +341,8 @@ function main() {
305
341
  }
306
342
  if (contentChanged) changedExisting++;
307
343
  const gate = promotionAllowed(c);
308
- registry.candidates[c.id] = { ...(prior ?? {}), ...c, first_seen_at: prior?.first_seen_at ?? runAt, last_seen_at: runAt, status: gate.ok ? "promotable" : "review_required", block_reason: gate.reason ?? null, content_state: contentChanged ? "changed" : "new" };
309
- if (!gate.ok) { blocked.push({ id: c.id, title: c.title, reason: gate.reason, allowlist_state: c.allowlist_state, category: c.category, competitor_labels: c.competitor_labels }); continue; }
344
+ registry.candidates[c.id] = { ...(prior ?? {}), ...c, first_seen_at: prior?.first_seen_at ?? runAt, last_seen_at: runAt, status: gate.ok ? "promotable" : "review_required", decision: gate.ok ? gate.reason : "stage_for_review", block_reason: gate.ok ? null : gate.reason, next_action: gate.ok ? "promote_on_apply" : "manual_review_required", content_state: contentChanged ? "changed" : "new" };
345
+ if (!gate.ok) { blocked.push({ id: c.id, title: c.title, kind: c.kind, source_type: c.source_type, reason: gate.reason, next_action: "manual_review_required", allowlist_state: c.allowlist_state, category: c.category, competitor_labels: c.competitor_labels }); continue; }
310
346
  planned.push(c);
311
347
  }
312
348
 
@@ -326,6 +362,7 @@ function main() {
326
362
 
327
363
  const graph = refreshGraph(args, promoted);
328
364
  const stale = registry.runs.at?.(-1)?.run_id ? [] : ["no_prior_apply_run_recorded"];
365
+ const reviewQueue = blocked.map((b) => ({ id: b.id, title: b.title, kind: b.kind, reason: b.reason, next_action: b.next_action })).slice(0, 50);
329
366
  const summary = {
330
367
  run_id: `kb-${Date.now()}`,
331
368
  last_run_at: runAt,
@@ -335,6 +372,7 @@ function main() {
335
372
  promoted_count: promoted,
336
373
  duplicate_skips: duplicates,
337
374
  blocked_count: blocked.length,
375
+ staged_count: blocked.length,
338
376
  skipped_count: skipped.length,
339
377
  failure_count: failed,
340
378
  changed_existing_count: changedExisting,
@@ -344,6 +382,8 @@ function main() {
344
382
  graph,
345
383
  exit_status: failed || graph.ok === false ? 1 : 0,
346
384
  promoted: promotedRefs,
385
+ review_queue_count: reviewQueue.length,
386
+ review_queue: reviewQueue,
347
387
  blocked: blocked.slice(0, 50),
348
388
  skipped: skipped.slice(0, 50),
349
389
  config: rel(cfg.path),
@@ -84,7 +84,7 @@ async function main() {
84
84
  const built = buildManifest(packageFiles, name, version);
85
85
 
86
86
  if (mode === "write") {
87
- await writeFile(MANIFEST_PATH, `${JSON.stringify(built, null, 2)}\n`, "utf-8");
87
+ await writeFile(MANIFEST_PATH, `${JSON.stringify(built, null, "\t")}\n`, "utf-8");
88
88
  console.log(
89
89
  `Wrote ${MANIFEST_PATH} (${Object.keys(built.agents).length} agents)`,
90
90
  );
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Toggle per-project harness governance — writes `.pi/harness/project.json`.
4
+ *
5
+ * Usage:
6
+ * node harness-project-toggle.mjs status [--project-root DIR]
7
+ * node harness-project-toggle.mjs enable [--project-root DIR]
8
+ * node harness-project-toggle.mjs disable [--project-root DIR]
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const CONFIG_BASENAME = "project.json";
16
+
17
+ function parseArgs(argv) {
18
+ const args = [...argv];
19
+ let projectRoot = process.cwd();
20
+ const positional = [];
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (arg === "--project-root" && args[i + 1]) {
24
+ projectRoot = args[++i];
25
+ continue;
26
+ }
27
+ positional.push(arg);
28
+ }
29
+ return { projectRoot, action: positional[0] ?? "status" };
30
+ }
31
+
32
+ function configPath(projectRoot) {
33
+ return join(projectRoot, ".pi", "harness", CONFIG_BASENAME);
34
+ }
35
+
36
+ function envOverrideEnabled() {
37
+ const raw = process.env.HARNESS_ENABLED?.trim().toLowerCase();
38
+ if (!raw) return null;
39
+ if (raw === "0" || raw === "false" || raw === "no") return false;
40
+ if (raw === "1" || raw === "true" || raw === "yes") return true;
41
+ return null;
42
+ }
43
+
44
+ function readConfig(projectRoot) {
45
+ const fromEnv = envOverrideEnabled();
46
+ if (fromEnv !== null) {
47
+ return {
48
+ schema_version: "1.0.0",
49
+ enabled: fromEnv,
50
+ source: "env:HARNESS_ENABLED",
51
+ };
52
+ }
53
+
54
+ const path = configPath(projectRoot);
55
+ if (!existsSync(path)) {
56
+ return {
57
+ schema_version: "1.0.0",
58
+ enabled: true,
59
+ source: "default",
60
+ };
61
+ }
62
+
63
+ try {
64
+ const raw = JSON.parse(readFileSync(path, "utf8"));
65
+ if (typeof raw.enabled === "boolean") {
66
+ return {
67
+ schema_version: "1.0.0",
68
+ enabled: raw.enabled,
69
+ updated_at: raw.updated_at,
70
+ source: path,
71
+ };
72
+ }
73
+ } catch {
74
+ // fall through
75
+ }
76
+
77
+ return {
78
+ schema_version: "1.0.0",
79
+ enabled: true,
80
+ source: "default-corrupt-file",
81
+ };
82
+ }
83
+
84
+ function writeConfig(projectRoot, enabled) {
85
+ const path = configPath(projectRoot);
86
+ mkdirSync(dirname(path), { recursive: true });
87
+ const payload = {
88
+ schema_version: "1.0.0",
89
+ enabled,
90
+ updated_at: new Date().toISOString(),
91
+ };
92
+ writeFileSync(path, `${JSON.stringify(payload, null, "\t")}\n`, "utf8");
93
+ return { ...payload, path };
94
+ }
95
+
96
+ function main() {
97
+ const { projectRoot, action } = parseArgs(process.argv.slice(2));
98
+ if (!["status", "enable", "disable"].includes(action)) {
99
+ console.error(
100
+ "Usage: harness-project-toggle.mjs <status|enable|disable> [--project-root DIR]",
101
+ );
102
+ process.exit(1);
103
+ }
104
+
105
+ if (action === "status") {
106
+ const config = readConfig(projectRoot);
107
+ console.log(JSON.stringify({ ok: true, projectRoot, ...config }, null, 2));
108
+ return;
109
+ }
110
+
111
+ const enabled = action === "enable";
112
+ const written = writeConfig(projectRoot, enabled);
113
+ console.log(
114
+ JSON.stringify(
115
+ {
116
+ ok: true,
117
+ projectRoot,
118
+ enabled: written.enabled,
119
+ path: written.path,
120
+ updated_at: written.updated_at,
121
+ reload_required: true,
122
+ },
123
+ null,
124
+ 2,
125
+ ),
126
+ );
127
+ }
128
+
129
+ main();