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.
- package/.agents/skills/harness-decisions/SKILL.md +1 -1
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.agents/skills/harness-review/SKILL.md +7 -7
- package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
- package/.agents/skills/harness-steer/SKILL.md +1 -1
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/agents/harness/planning/decompose.md +1 -1
- package/.pi/extensions/00-harness-project-control.ts +133 -0
- package/.pi/extensions/budget-guard.ts +2 -0
- package/.pi/extensions/debate-orchestrator.ts +2 -0
- package/.pi/extensions/harness-ask-user.ts +2 -2
- package/.pi/extensions/harness-debate-tools.ts +2 -2
- package/.pi/extensions/harness-live-widget.ts +33 -2
- package/.pi/extensions/harness-plan-approval.ts +2 -2
- package/.pi/extensions/harness-run-context.ts +180 -12
- package/.pi/extensions/harness-subagent-submit.ts +3 -2
- package/.pi/extensions/harness-subagents.ts +2 -2
- package/.pi/extensions/harness-telemetry.ts +2 -0
- package/.pi/extensions/harness-web-tools.ts +2 -2
- package/.pi/extensions/lib/extension-load-guard.ts +10 -0
- package/.pi/extensions/lib/harness-artifact-gate.ts +5 -15
- package/.pi/extensions/lib/harness-spawn-topology.ts +4 -27
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -2
- package/.pi/extensions/lib/harness-subagent-policy.ts +5 -5
- package/.pi/extensions/lib/harness-subagent-precheck.ts +3 -3
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +3 -21
- package/.pi/extensions/lib/plan-approval-readiness.ts +3 -52
- package/.pi/extensions/lib/spawn-policy.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +2 -0
- package/.pi/extensions/review-integrity.ts +91 -10
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/test-diff-integrity.ts +1 -0
- package/.pi/extensions/trace-recorder.ts +2 -0
- package/.pi/harness/agents.manifest.json +23 -31
- package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/README.md +1 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
- package/.pi/harness/docs/practice-map.md +2 -2
- package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-run-context.ts +1 -1
- package/.pi/lib/harness-ui-state.ts +27 -12
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-critic.md +1 -1
- package/.pi/prompts/harness-plan.md +3 -5
- package/.pi/prompts/harness-review.md +9 -9
- package/.pi/prompts/harness-run.md +7 -7
- package/.pi/prompts/harness-setup.md +5 -4
- package/.pi/prompts/harness-steer.md +2 -2
- package/.pi/scripts/README.md +1 -0
- package/.pi/scripts/graphify-kb-updater.mjs +48 -8
- package/.pi/scripts/harness-agents-manifest.mjs +1 -1
- package/.pi/scripts/harness-project-toggle.mjs +129 -0
- package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
- package/CHANGELOG.md +12 -0
- package/README.md +94 -58
- package/package.json +3 -3
- package/.pi/agents/harness/planning/scout-graphify.md +0 -39
- package/.pi/agents/harness/planning/scout-semantic.md +0 -41
- package/.pi/agents/harness/planning/scout-structure.md +0 -37
- /package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -0
- /package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -0
- /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
- /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 "
|
|
385
|
+
return "run";
|
|
388
386
|
case "evaluate":
|
|
389
|
-
return "eval";
|
|
390
387
|
case "adversary":
|
|
391
|
-
return "review";
|
|
392
388
|
case "merge":
|
|
393
|
-
return "
|
|
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
|
-
|
|
448
|
+
const mainPhaseCommand = mainPhaseCommandForStatus(state);
|
|
449
|
+
if (mainPhaseCommand) {
|
|
434
450
|
return {
|
|
435
|
-
text: `Next: ${truncateStatusCommand(
|
|
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: "
|
|
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: "
|
|
472
|
+
return { text: "Reviewing changes", severity: "accent" };
|
|
458
473
|
case "merge":
|
|
459
|
-
return { text: "
|
|
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;
|
|
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.
|
|
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"] })
|
|
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
|
|
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`),
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
package/.pi/scripts/README.md
CHANGED
|
@@ -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
|
|
183
|
-
candidates.push({ ...entry, kind
|
|
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.
|
|
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",
|
|
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,
|
|
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();
|