nccgs 2.0.0-rc.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/nccgs/MEMORY.md +7 -2
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/packages/ponytail/README.md +2 -2
- package/.claude/nccgs/protocols/agent-contract.md +15 -2
- package/.claude/nccgs/protocols/bounded-repair.md +135 -7
- package/.claude/nccgs/protocols/editor-session-evidence.md +52 -0
- package/.claude/nccgs/protocols/orchestration.md +7 -5
- package/.claude/nccgs/protocols/review-handoff.md +17 -1
- package/.claude/nccgs/protocols/standalone-gates.md +64 -8
- package/.claude/nccgs/protocols/standalone.md +33 -13
- package/.claude/nccgs/protocols/task-gates.md +13 -1
- package/.claude/nccgs/protocols/task-slices.md +106 -0
- package/.claude/nccgs/routing.generated.md +20 -0
- package/.claude/nccgs/tools/command-process.mjs +10 -4
- package/.claude/nccgs/tools/compile-policy.mjs +8 -0
- package/.claude/nccgs/tools/configure-models.mjs +4 -0
- package/.claude/nccgs/tools/editor-session-evidence.mjs +66 -0
- package/.claude/nccgs/tools/gates.mjs +7 -4
- package/.claude/nccgs/tools/handoff.mjs +51 -0
- package/.claude/nccgs/tools/inputs.mjs +36 -1
- package/.claude/nccgs/tools/installation-guard.mjs +55 -0
- package/.claude/nccgs/tools/lifecycle.mjs +21 -0
- package/.claude/nccgs/tools/orchestration.mjs +264 -14
- package/.claude/nccgs/tools/repair-packet.mjs +29 -10
- package/.claude/nccgs/tools/returned-handoff.mjs +38 -0
- package/.claude/nccgs/tools/review-budget.mjs +171 -0
- package/.claude/nccgs/tools/review-handoff.mjs +42 -8
- package/.claude/nccgs/tools/runtime.mjs +7 -2
- package/.claude/nccgs/tools/session-budget.mjs +67 -0
- package/.claude/nccgs/tools/task-slices.mjs +147 -0
- package/.claude/nccgs/tools/task.mjs +39 -2
- package/.claude/nccgs/tools/tool-guard.mjs +19 -5
- package/.claude/nccgs/tools/unity-repair.mjs +128 -0
- package/.claude/nccgs/tools/verification-jobs.mjs +13 -3
- package/.claude/nccgs/tools/windows-compiler-server.cs +163 -0
- package/.claude/nccgs/tools/windows-job.cs +8 -1
- package/CHANGELOG.md +142 -0
- package/MEMORY.md +7 -2
- package/README.md +57 -27
- package/UPGRADING.md +27 -18
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +45 -18
- package/docs/BOUNDED-REPAIR.md +76 -7
- package/docs/EDITOR-SESSION-EVIDENCE.md +52 -0
- package/docs/HANDOFF-AUTOMATION.md +144 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +68 -11
- package/docs/PHASE-1-OPERATIONS.md +142 -1
- package/docs/PHASE-2-OPERATIONS.md +1 -1
- package/docs/RELEASE-STATUS.md +102 -4
- package/docs/TASK-SLICING.md +57 -0
- package/docs/UNINSTALL.md +134 -0
- package/docs/UPGRADE-QUALIFICATION.md +1 -1
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +2 -2
- package/scaffold/.nccgs/templates/repair-unity-code.json +18 -0
- package/scaffold/.nccgs/templates/repair-unity-editor.json +21 -0
- package/scaffold/.nccgs/templates/task-slices.json +28 -0
- package/scripts/cli.mjs +15 -2
- package/scripts/install.mjs +14 -0
- package/scripts/recover-session.mjs +89 -0
- package/scripts/run.mjs +21 -8
- package/scripts/supervise.mjs +30 -6
- package/scripts/uninstall.mjs +292 -0
- package/scripts/validate.mjs +6 -0
- package/scripts/work-handoff.mjs +38 -0
- package/tests/editor-session-evidence.test.mjs +45 -0
- package/tests/fixtures/compiler-shutdown.cs +64 -0
- package/tests/orchestration-v15.test.mjs +52 -1
- package/tests/phase1.test.mjs +92 -0
- package/tests/policy-v15.test.mjs +14 -0
- package/tests/review-budget.test.mjs +177 -0
- package/tests/runtime-v15.test.mjs +572 -4
- package/tests/session-budget.test.mjs +34 -0
- package/tests/task-slices.test.mjs +285 -0
- package/tests/uninstall.test.mjs +477 -0
- package/tests/unity-generated.test.mjs +140 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Remove NCCGS from a project
|
|
2
|
+
|
|
3
|
+
`uninstall` is available in the **unpublished 2.1.0 package**. The published
|
|
4
|
+
2.0.0 CLI does not have this command. Use the local 2.1.0 archive (or its source
|
|
5
|
+
CLI) to remove an existing 2.0.0 installation; no preliminary update is needed.
|
|
6
|
+
|
|
7
|
+
Close Claude Code sessions and let managed workers/verification finish first.
|
|
8
|
+
Run from an external terminal. The command checks recorded process ownership,
|
|
9
|
+
dispatches, runs and jobs; it never kills processes, settles jobs or closes tasks.
|
|
10
|
+
Sessions launched directly with `claude`, or with older launchers, may have no
|
|
11
|
+
session record, so they must also be closed by the operator.
|
|
12
|
+
|
|
13
|
+
## Preview and remove
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
$NccgsArchive = "D:\path\to\nccgs-2.1.0.tgz"
|
|
17
|
+
npx --yes --package "$NccgsArchive" nccgs uninstall --project "D:\path\to\UnityProject" --dry-run
|
|
18
|
+
npx --yes --package "$NccgsArchive" nccgs uninstall --project "D:\path\to\UnityProject"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From this source checkout, use `node scripts/cli.mjs uninstall --project DIR`
|
|
22
|
+
with the same options. Always name the intended project. The package refuses
|
|
23
|
+
to uninstall its own source directory.
|
|
24
|
+
|
|
25
|
+
The preview writes nothing and lists files to remove, shared files to edit and
|
|
26
|
+
modified/unknown inventoried files to preserve. There is no `--force` or history
|
|
27
|
+
purge option. A repeated uninstall reports `NOT_INSTALLED` after successful removal.
|
|
28
|
+
|
|
29
|
+
| Item | Result |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Unchanged inventoried NCCGS/Unity skills, agents, protocols and tools | Removed after fingerprint checks |
|
|
32
|
+
| Optional generated NCCGS agents | Removed only if their recorded fingerprints still match |
|
|
33
|
+
| Customized or unknown managed files; unrelated files in `.claude` | Kept in place |
|
|
34
|
+
| Exact NCCGS hooks and canonical import in `CLAUDE.md` | Removed; unrelated hooks and owner text kept |
|
|
35
|
+
| Unchanged NCCGS bootstrap `CLAUDE.md` | Removed |
|
|
36
|
+
| Game source, scenes, prefabs, packages and project settings | Unchanged |
|
|
37
|
+
| `.nccgs` policy, task history, reviews, evidence, budgets and prior backups | Kept byte for byte |
|
|
38
|
+
| Generic Claude permission/model preferences | Kept; older installers did not track their ownership |
|
|
39
|
+
|
|
40
|
+
A retained custom NCCGS skill or agent may still be discoverable by Claude and
|
|
41
|
+
may refer to removed tools. The output identifies those files for manual review;
|
|
42
|
+
they are not silently deleted. Unknown non-inventoried owner files are never
|
|
43
|
+
claimed as NCCGS-owned. Customized compound hooks/imports must be separated by
|
|
44
|
+
the owner before removal, because automatically editing them could lose behavior.
|
|
45
|
+
|
|
46
|
+
## Backup and recovery
|
|
47
|
+
|
|
48
|
+
Removal prints an `id` and saves a manifest plus original file bytes beneath
|
|
49
|
+
`.nccgs/uninstall/<id>/`. Keep that directory if you may need recovery.
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
npx --yes --package "$NccgsArchive" nccgs uninstall --project "D:\path\to\UnityProject" --rollback "PRINTED-UUID" --dry-run
|
|
53
|
+
npx --yes --package "$NccgsArchive" nccgs uninstall --project "D:\path\to\UnityProject" --rollback "PRINTED-UUID"
|
|
54
|
+
npx --yes --package "$NccgsArchive" nccgs doctor --project "D:\path\to\UnityProject"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use the same package for recovery. All backup hashes and destination files are
|
|
58
|
+
checked before restoration; a later edit or damaged backup causes refusal. The
|
|
59
|
+
receipt and history remain after restoration. A write failure attempts automatic
|
|
60
|
+
restoration; an interrupted operation requires inspection and explicit rollback.
|
|
61
|
+
New launch/install/update operations refuse an incomplete uninstall receipt.
|
|
62
|
+
Do not delete locks or receipts merely to bypass this check.
|
|
63
|
+
|
|
64
|
+
Agent reconfiguration, execution migration and policy regeneration also participate
|
|
65
|
+
in the uninstall admission lock. New cooperating operations cannot register a
|
|
66
|
+
session while removal/rollback holds this lock. A refusal before any managed file
|
|
67
|
+
is changed leaves a `cancelled` receipt with its diagnostic and backups; it does
|
|
68
|
+
not require rollback. After changes begin, normal ownership checks still apply to
|
|
69
|
+
restoration. Unrecorded/older concurrent operations require operator inspection.
|
|
70
|
+
Failure to remove an empty directory is a warning after successful removal; the
|
|
71
|
+
result still returns the backup path and rollback ID.
|
|
72
|
+
|
|
73
|
+
Two launches/configuration commands can briefly contend for the admission lock.
|
|
74
|
+
This reports `installation admission is busy or unverified`, rather than claiming
|
|
75
|
+
an uninstall is running. Retry once the other operation finishes; the command
|
|
76
|
+
does not retry automatically or take over a live/unverified lock.
|
|
77
|
+
Empty, incomplete, unreadable or disappearing lock records also produce the
|
|
78
|
+
busy/unverified diagnostic, retaining the original cause for inspection. They
|
|
79
|
+
never authorize admission or automatic recovery. A holder that releases before
|
|
80
|
+
the error recheck still results in refusal; retry explicitly once work settles.
|
|
81
|
+
|
|
82
|
+
## Recover a crashed launcher lease
|
|
83
|
+
|
|
84
|
+
Closing a terminal forcibly or a crash can leave a `nccgs run` lease in
|
|
85
|
+
`.nccgs/sessions/<UUID>.lock`. An unmanaged child's descendants cannot be proven
|
|
86
|
+
stopped merely because its launcher and child PIDs are dead. Uninstall therefore
|
|
87
|
+
continues to refuse this unknown state automatically.
|
|
88
|
+
|
|
89
|
+
The external operator can explicitly reconcile **only that launcher lease**:
|
|
90
|
+
|
|
91
|
+
```powershell
|
|
92
|
+
npx --yes --package "$NccgsArchive" nccgs recover-session --project "D:\path\to\UnityProject" --id "SESSION-UUID" --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Independently inspect and stop the entire session process tree first, including
|
|
96
|
+
any child that spawned before its PID was recorded. Rebooting the host and checking
|
|
97
|
+
that the session has not restarted is one way to establish that boundary. Only
|
|
98
|
+
when this is true, use the preview's SHA-256 and record how it was confirmed:
|
|
99
|
+
|
|
100
|
+
```powershell
|
|
101
|
+
npx --yes --package "$NccgsArchive" nccgs recover-session --project "D:\path\to\UnityProject" --id "SESSION-UUID" --sha256 "HASH-FROM-PREVIEW" --confirm-process-tree-stopped --reason "Describe how the full session tree was confirmed stopped"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This is a human/operator attestation, **not automatic descendant proof**. The
|
|
105
|
+
command refuses live/unverified recorded PIDs, a foreign host, changed hashes,
|
|
106
|
+
wrong operation, unsafe IDs and invocation inside Claude Code. It archives the
|
|
107
|
+
exact original lease and a receipt with the operator's reason under
|
|
108
|
+
`.nccgs/session-recoveries/<id>/`. It never kills a process or edits task/job/run
|
|
109
|
+
locks, verdicts, history or budgets. Other active/unknown work still blocks
|
|
110
|
+
uninstall. A missing child PID during `spawning` explicitly remains unobserved
|
|
111
|
+
in the receipt; the operator must establish shutdown independently before confirming.
|
|
112
|
+
Keep the receipts and retry the original uninstall/rollback only after all work
|
|
113
|
+
has settled. There is no age-based or automatic eviction of unknown leases.
|
|
114
|
+
|
|
115
|
+
Each recovery writes an immutable `receipt.json` with status `pending` before
|
|
116
|
+
moving the lease, then a separate `outcome.json` with status `completed` or
|
|
117
|
+
`failed`. The outcome identifies the pending receipt by SHA-256. A rename failure
|
|
118
|
+
leaves the original lease in place and records the error; it is not a successful
|
|
119
|
+
recovery. If a crash or write failure leaves no valid outcome, completion remains
|
|
120
|
+
unconfirmed: inspect both the original lease and the archive, retaining every
|
|
121
|
+
record. The lease may already have moved if recording completion failed. A pending
|
|
122
|
+
receipt alone never proves success, and the command does not report success when
|
|
123
|
+
it cannot record completion. Retrying a failed move creates a new receipt; it
|
|
124
|
+
does not overwrite the earlier attempt.
|
|
125
|
+
|
|
126
|
+
Reinstalling later reuses retained policy and task records. It does not replenish
|
|
127
|
+
spent money, turns, reviews or time. Restoring an old uninstall must not overwrite
|
|
128
|
+
a subsequent installation. Removal/rollback is a maintenance action, not a task
|
|
129
|
+
PASS, independent review, acceptance decision or a new allowance.
|
|
130
|
+
|
|
131
|
+
The command removes the project integration only. It does not uninstall Claude
|
|
132
|
+
Code, Unity, Node.js, global npm packages or npm's cache, and makes no provider
|
|
133
|
+
calls. Symlinks/junctions, malformed records and uncertain process ownership fail
|
|
134
|
+
closed rather than permitting deletion outside the checked project.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Published 1.3.0 upgrade qualification
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Historical target: **2.0.0-rc.3**. This is installation/configuration qualification on Windows
|
|
4
4
|
with Node.js 24.12.0 using the published npm 1.3.0 archive and the exact RC3 archive.
|
|
5
5
|
It does not establish Node 18, macOS/Linux, provider execution or gameplay results.
|
|
6
6
|
|
package/docs/WORKFLOWS.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nccgs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Unity-first game-studio operating system for Claude Code",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public",
|
|
31
|
-
"tag": "
|
|
31
|
+
"tag": "latest",
|
|
32
32
|
"registry": "https://registry.npmjs.org/"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"repairType": "unity-code",
|
|
4
|
+
"taskId": "REPLACE_WITH_TASK_ID",
|
|
5
|
+
"contractHash": "REPLACE_WITH_CURRENT_CONTRACT_HASH",
|
|
6
|
+
"sourceSnapshot": "REPLACE_WITH_CURRENT_SOURCE_SNAPSHOT",
|
|
7
|
+
"problem": "Describe the recorded failure and its evidence.",
|
|
8
|
+
"solution": "State the chosen correction, relevant callers and regression coverage.",
|
|
9
|
+
"changes": [
|
|
10
|
+
{ "path": "Assets/REPLACE_WITH_SOURCE.cs", "operation": "edit", "instruction": "Apply the exact chosen correction; preserve the full acceptance contract." }
|
|
11
|
+
],
|
|
12
|
+
"context": [],
|
|
13
|
+
"verification": ["REPLACE_WITH_REQUIRED_CHECK_NAME"],
|
|
14
|
+
"unity": {
|
|
15
|
+
"steps": []
|
|
16
|
+
},
|
|
17
|
+
"budget": { "workCalls": 24, "readCalls": 12, "handoffCalls": 3, "durationMs": 900000 }
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"repairType": "unity-editor",
|
|
4
|
+
"taskId": "REPLACE_WITH_TASK_ID",
|
|
5
|
+
"contractHash": "REPLACE_WITH_CURRENT_CONTRACT_HASH",
|
|
6
|
+
"sourceSnapshot": "REPLACE_WITH_CURRENT_SOURCE_SNAPSHOT",
|
|
7
|
+
"problem": "Describe the recorded scene, prefab or asset failure.",
|
|
8
|
+
"solution": "State the chosen Editor operation and how it saves only the exact affected files.",
|
|
9
|
+
"changes": [
|
|
10
|
+
{ "path": "Assets/REPLACE_WITH_PREFAB.prefab", "operation": "edit", "instruction": "Apply and save the chosen correction through Unity Editor APIs." }
|
|
11
|
+
],
|
|
12
|
+
"context": [],
|
|
13
|
+
"verification": ["REPLACE_WITH_REQUIRED_CHECK_NAME"],
|
|
14
|
+
"unity": {
|
|
15
|
+
"capabilities": { "path": ".nccgs/evidence/REPLACE_WITH_UNITY_DISCOVERY.json", "sha256": "REPLACE_WITH_CAPTURED_HASH" },
|
|
16
|
+
"steps": [
|
|
17
|
+
{ "id": "apply-and-save", "command": "eval", "arguments": ["REPLACE_WITH_CHOSEN_CSHARP_BODY_USING_FULLY_QUALIFIED_TYPES"], "timeoutMs": 30000 }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"budget": { "workCalls": 20, "readCalls": 8, "handoffCalls": 3, "durationMs": 900000 }
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"taskId": "REPLACE_WITH_EXISTING_TASK_ID",
|
|
4
|
+
"contractHash": "REPLACE_WITH_CURRENT_CONTRACT_HASH",
|
|
5
|
+
"sourceSnapshot": "REPLACE_WITH_CURRENT_SOURCE_SNAPSHOT",
|
|
6
|
+
"planId": "delivery-one",
|
|
7
|
+
"supersedes": null,
|
|
8
|
+
"reason": "Split the existing contract into bounded assignments within its remaining budget.",
|
|
9
|
+
"integration": "integrate",
|
|
10
|
+
"slices": [
|
|
11
|
+
{
|
|
12
|
+
"id": "foundation",
|
|
13
|
+
"objective": "Implement the first coherent behavior and its targeted test.",
|
|
14
|
+
"agent": "nccgs-gameplay-programmer",
|
|
15
|
+
"dependsOn": [],
|
|
16
|
+
"writePaths": ["Assets/REPLACE_WITH_FIRST_SOURCE.cs", "Assets/Tests/REPLACE_WITH_FIRST_TEST.cs"],
|
|
17
|
+
"criteria": ["REPLACE_WITH_ORIGINAL_CRITERION_ID"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "integrate",
|
|
21
|
+
"objective": "Implement the remaining behavior and integrate the full original contract.",
|
|
22
|
+
"agent": "nccgs-gameplay-programmer",
|
|
23
|
+
"dependsOn": ["foundation"],
|
|
24
|
+
"writePaths": ["Assets/REPLACE_WITH_SECOND_SOURCE.cs", "Assets/Tests/REPLACE_WITH_INTEGRATION_TEST.cs"],
|
|
25
|
+
"criteria": ["REPLACE_WITH_REMAINING_ORIGINAL_CRITERION_ID"]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
package/scripts/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import crypto from "node:crypto";
|
|
8
8
|
import { atomicJson, contained, executionPolicy, hash, json, profilePolicy, resolveExecution } from "../.claude/nccgs/tools/policy.mjs";
|
|
9
|
+
import { acquireInstallationLease, assertInstallationAvailable } from "../.claude/nccgs/tools/installation-guard.mjs";
|
|
9
10
|
|
|
10
11
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const root = path.resolve(scriptDir, "..");
|
|
@@ -29,13 +30,13 @@ if (command === "run") {
|
|
|
29
30
|
catch (error) { console.error(`Execution migration failed: ${error.message}`); process.exitCode = 1; }
|
|
30
31
|
} else {
|
|
31
32
|
|
|
32
|
-
if (!new Set(["install", "update", "doctor", "task", "agents", "policy"]).has(command)) {
|
|
33
|
+
if (!new Set(["install", "update", "uninstall", "recover-session", "doctor", "task", "agents", "policy"]).has(command)) {
|
|
33
34
|
console.error(`Unknown command: ${command}`);
|
|
34
35
|
printHelp();
|
|
35
36
|
process.exit(2);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
const scripts = { doctor: "doctor.mjs", task: "../.claude/nccgs/tools/task.mjs", agents: "../.claude/nccgs/tools/configure-models.mjs", policy: "../.claude/nccgs/tools/compile-policy.mjs" };
|
|
39
|
+
const scripts = { uninstall: "uninstall.mjs", "recover-session": "recover-session.mjs", doctor: "doctor.mjs", task: "../.claude/nccgs/tools/task.mjs", agents: "../.claude/nccgs/tools/configure-models.mjs", policy: "../.claude/nccgs/tools/compile-policy.mjs" };
|
|
39
40
|
const script = scripts[command] || "install.mjs";
|
|
40
41
|
let forwardedArgs = command === "update" ? ["--update-only", ...args] : args;
|
|
41
42
|
if (command === "agents" && !args.includes("--project")) forwardedArgs = ["--project", process.cwd(), ...args];
|
|
@@ -66,6 +67,9 @@ function migrateExecution() {
|
|
|
66
67
|
if (!allowed.has(args[i])) throw new Error(`Unknown migration option: ${args[i]}`);
|
|
67
68
|
if (args[i] !== "--dry-run") i++;
|
|
68
69
|
}
|
|
70
|
+
assertInstallationAvailable(target);
|
|
71
|
+
const lease = dryRun ? null : acquireInstallationLease(target, "nccgs-migrate-execution");
|
|
72
|
+
try {
|
|
69
73
|
const at = relative => contained(target, relative);
|
|
70
74
|
if (!fs.existsSync(at(".claude/nccgs/install-state.json"))) throw new Error("Install/update NCCGS before migrating execution");
|
|
71
75
|
const studio = json(at(".claude/nccgs/studio.json"));
|
|
@@ -158,6 +162,7 @@ function migrateExecution() {
|
|
|
158
162
|
throw error;
|
|
159
163
|
}
|
|
160
164
|
console.log(`Execution migration ${id} applied. Rollback: nccgs migrate-execution --rollback ${id} --dry-run`);
|
|
165
|
+
} finally { lease?.release(); }
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
function printHelp() {
|
|
@@ -166,11 +171,19 @@ function printHelp() {
|
|
|
166
171
|
Usage:
|
|
167
172
|
nccgs install [options]
|
|
168
173
|
nccgs update [options]
|
|
174
|
+
nccgs uninstall [--project DIR] [--dry-run]
|
|
175
|
+
nccgs uninstall --rollback UNINSTALL_ID [--project DIR] [--dry-run]
|
|
176
|
+
nccgs recover-session --id SESSION_UUID [--project DIR] --dry-run
|
|
177
|
+
nccgs recover-session --id SESSION_UUID --sha256 HASH --confirm-process-tree-stopped --reason TEXT [--project DIR]
|
|
169
178
|
nccgs doctor [options]
|
|
170
179
|
nccgs run [--project DIR] [--claude EXECUTABLE] [--dry-run] -- [Claude arguments/prompt]
|
|
171
180
|
nccgs run --supervise --task ID --prompt-file FILE --session-budget-usd N [--max-resumes 2] [--timeout-ms 3600000] -- [provider options]
|
|
172
181
|
nccgs run --recover-run RUN_ID [--project DIR] [--claude EXECUTABLE]
|
|
173
182
|
nccgs task init|snapshot|ref|observe|verify|acceptance-status|record|check|close|bind|extend-review|refresh-inputs|migrate-evidence [options]
|
|
183
|
+
nccgs task dispatch-prompt|handoff-validate|record-review --id TASK --dispatch ID [--file HANDOFF]
|
|
184
|
+
nccgs task repair-editor --id TASK --dispatch ID --step STEP (admitted Unity repair worker only)
|
|
185
|
+
nccgs task slice-preview|slice-plan --id TASK --file PLAN (same task and budget)
|
|
186
|
+
nccgs task slice-status --id TASK
|
|
174
187
|
nccgs agents --enable <role> | --disable <role> [--project <directory>]
|
|
175
188
|
nccgs policy [--write] Check/regenerate package routing
|
|
176
189
|
nccgs migrate-execution --to standalone [--external-review off|advisory|required] [--model-profile <profile>] [--dry-run]
|
package/scripts/install.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { assertProjectRoot, assertRelativePath, containedPath } from "../.claude/nccgs/tools/paths.mjs";
|
|
12
12
|
import { executionPolicy } from "../.claude/nccgs/tools/policy.mjs";
|
|
13
13
|
import { autonomyPolicy, canonicalHookCommand } from "../.claude/nccgs/tools/autonomy.mjs";
|
|
14
|
+
import { acquireInstallationLease, assertInstallationAvailable } from "../.claude/nccgs/tools/installation-guard.mjs";
|
|
14
15
|
|
|
15
16
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const root = path.resolve(scriptDir, "..");
|
|
@@ -43,6 +44,8 @@ if (project === root) {
|
|
|
43
44
|
console.error("The NCCGS package directory cannot be its own installation target.");
|
|
44
45
|
process.exit(2);
|
|
45
46
|
}
|
|
47
|
+
try { assertInstallationAvailable(project); }
|
|
48
|
+
catch (error) { console.error(error.message); process.exit(2); }
|
|
46
49
|
|
|
47
50
|
const unityMarkers = ["Assets", "Packages", "ProjectSettings"];
|
|
48
51
|
const missingMarkers = unityMarkers.filter((item) => !fs.existsSync(projectPath(item)));
|
|
@@ -63,6 +66,17 @@ if (validation.status !== 0) {
|
|
|
63
66
|
process.exit(1);
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
// Register before reading the installation to keep a concurrent uninstall from
|
|
70
|
+
// invalidating the plan. The second guard closes the acquisition race. Dry-run
|
|
71
|
+
// remains read-only; the exit handler covers all existing early exit paths.
|
|
72
|
+
if (!dryRun) {
|
|
73
|
+
try {
|
|
74
|
+
const lease = acquireInstallationLease(project, "nccgs-install");
|
|
75
|
+
process.once("exit", () => lease.release());
|
|
76
|
+
assertInstallationAvailable(project);
|
|
77
|
+
} catch (error) { console.error(error.message); process.exit(2); }
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
const frameworkSource = path.join(root, ".claude");
|
|
67
81
|
const frameworkFiles = walk(frameworkSource).map((file) => ({
|
|
68
82
|
source: file,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertProjectRoot, containedPath } from "../.claude/nccgs/tools/paths.mjs";
|
|
7
|
+
import { acquireLock, pidState } from "../.claude/nccgs/tools/process-lock.mjs";
|
|
8
|
+
import { sha256 } from "../.claude/nccgs/tools/integrity.mjs";
|
|
9
|
+
|
|
10
|
+
const need = (ok, message) => { if (!ok) throw new Error(message); };
|
|
11
|
+
const uuid = value => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value);
|
|
12
|
+
|
|
13
|
+
// Explicit operator reconciliation, never automatic proof that an unmanaged
|
|
14
|
+
// descendant tree stopped. The immutable receipt says which facts are observed
|
|
15
|
+
// and which are attested; ownerState and all task/job/run locks stay unchanged.
|
|
16
|
+
export function recoverSession(project, { id, expectedSha256, confirmStopped = false, reason, dryRun = false } = {}) {
|
|
17
|
+
project = assertProjectRoot(project);
|
|
18
|
+
need(!(process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.NCCGS_SUPERVISED_SESSION), "Session recovery requires an external operator terminal.");
|
|
19
|
+
need(uuid(id), "Use the UUID from .nccgs/sessions/<UUID>.lock.");
|
|
20
|
+
const relative = `.nccgs/sessions/${id}.lock`, at = name => containedPath(project, name);
|
|
21
|
+
const inspect = () => {
|
|
22
|
+
const bytes = fs.readFileSync(at(relative)), record = JSON.parse(bytes.toString("utf8"));
|
|
23
|
+
need(record.schemaVersion === 1 && record.operation === "nccgs-run" && record.host === os.hostname()
|
|
24
|
+
&& uuid(record.token) && ["owned", "spawning", "running"].includes(record.phase), "Only a valid local nccgs-run session lease can be reconciled.");
|
|
25
|
+
need(pidState(record.pid) === "dead", "Session launcher is live or unverified; stop/inspect it first.");
|
|
26
|
+
if (record.child !== null) need(record.child && pidState(record.child.pid) === "dead", "Recorded child is live or unverified; stop/inspect it first.");
|
|
27
|
+
return { bytes, record, hash: sha256(bytes) };
|
|
28
|
+
};
|
|
29
|
+
const preview = inspect();
|
|
30
|
+
if (dryRun) return { status: "RECOVERY_PREVIEW", project, id, sha256: preview.hash, launcherPid: preview.record.pid,
|
|
31
|
+
childPid: preview.record.child?.pid ?? null, observedPids: "dead", descendantTree: "unverified",
|
|
32
|
+
required: "Independently stop/verify the entire session process tree (including any spawn without a recorded PID), then provide --sha256, --confirm-process-tree-stopped and --reason. PID death alone is insufficient." };
|
|
33
|
+
need(typeof expectedSha256 === "string" && /^[a-f0-9]{64}$/.test(expectedSha256) && preview.hash === expectedSha256, "Lease changed or --sha256 does not match the preview.");
|
|
34
|
+
need(confirmStopped && typeof reason === "string" && reason.trim().length >= 10 && reason.length <= 2000,
|
|
35
|
+
"Require --confirm-process-tree-stopped and a specific --reason recording how the entire process tree was confirmed stopped; this cannot be inferred from dead PIDs.");
|
|
36
|
+
const gate = acquireLock(at(".nccgs/uninstall.lock"), { operation: "session-reconciliation" });
|
|
37
|
+
try {
|
|
38
|
+
const current = inspect(); need(current.hash === expectedSha256, "Lease changed after preview.");
|
|
39
|
+
const recoveryId = crypto.randomUUID(), directory = `.nccgs/session-recoveries/${recoveryId}`;
|
|
40
|
+
fs.mkdirSync(at(directory), { recursive: true });
|
|
41
|
+
const receipt = { schemaVersion: 1, kind: "operator-session-reconciliation", status: "pending", outcomeFile: "outcome.json", id: recoveryId, sessionId: id,
|
|
42
|
+
at: new Date().toISOString(), host: os.hostname(), operator: os.userInfo().username, originalPath: relative,
|
|
43
|
+
originalSha256: current.hash, originalToken: current.record.token,
|
|
44
|
+
observed: { launcherPid: current.record.pid, launcherState: "dead", childPid: current.record.child?.pid ?? null, childState: current.record.child ? "dead" : "not-recorded" },
|
|
45
|
+
attestation: { entireProcessTreeStopped: true, reason: reason.trim(), automaticDescendantProof: false },
|
|
46
|
+
history: "Intent: archive only this launcher lease. This pending receipt does not prove completion. No process is killed; no task, job, run, verdict or budget is modified." };
|
|
47
|
+
const receiptText = JSON.stringify(receipt, null, 2) + "\n";
|
|
48
|
+
fs.writeFileSync(at(`${directory}/receipt.json`), receiptText, { flag: "wx" });
|
|
49
|
+
const writeOutcome = (status, details) => fs.writeFileSync(at(`${directory}/outcome.json`), JSON.stringify({
|
|
50
|
+
schemaVersion: 1, kind: "operator-session-reconciliation-outcome", id: recoveryId, sessionId: id,
|
|
51
|
+
at: new Date().toISOString(), receiptSha256: sha256(receiptText), status, ...details
|
|
52
|
+
}, null, 2) + "\n", { flag: "wx" });
|
|
53
|
+
try {
|
|
54
|
+
need(inspect().hash === expectedSha256, "Lease changed during reconciliation; retained receipt did not release it.");
|
|
55
|
+
// Both resolved paths are inside the project and checked for links/aliases.
|
|
56
|
+
// Rename archives the exact original bytes instead of deleting the record.
|
|
57
|
+
fs.renameSync(at(relative), at(`${directory}/lease.lock`));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
let auditError = "";
|
|
60
|
+
try { writeOutcome("failed", { error: error.message, code: error.code ?? null }); }
|
|
61
|
+
catch (recordError) { auditError = ` Outcome could not be recorded: ${recordError.message}; completion remains unconfirmed.`; }
|
|
62
|
+
throw new Error(`Session recovery ${recoveryId} failed: ${error.message}. Inspect ${directory}; keep its pending receipt.${auditError}`);
|
|
63
|
+
}
|
|
64
|
+
try { writeOutcome("completed", { archivedLease: `${directory}/lease.lock`, originalSha256: current.hash }); }
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error(`Session recovery ${recoveryId}: lease was archived, but completion could not be recorded: ${error.message}. Inspect ${directory}; the pending receipt alone is not completion evidence.`);
|
|
67
|
+
}
|
|
68
|
+
return { status: "SESSION_RECONCILED", id, recoveryId, receipt: `${directory}/receipt.json`, outcome: `${directory}/outcome.json`, archivedLease: `${directory}/lease.lock`,
|
|
69
|
+
scope: "Explicit operator attestation, not automatic descendant proof. Other active/unknown records continue to block uninstall." };
|
|
70
|
+
} finally { gate.release(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
74
|
+
try {
|
|
75
|
+
const options = {}, args = process.argv.slice(2), seen = new Set(); let project = process.cwd();
|
|
76
|
+
const values = { "--id": "id", "--sha256": "expectedSha256", "--reason": "reason" };
|
|
77
|
+
for (let i = 0; i < args.length; i++) {
|
|
78
|
+
const option = args[i]; need(!seen.has(option), `Duplicate option: ${option}`); seen.add(option);
|
|
79
|
+
if (option === "--dry-run") options.dryRun = true;
|
|
80
|
+
else if (option === "--confirm-process-tree-stopped") options.confirmStopped = true;
|
|
81
|
+
else {
|
|
82
|
+
need(option === "--project" || Object.hasOwn(values, option), `Unknown recovery option: ${option}`);
|
|
83
|
+
const value = args[++i]; need(value && !value.startsWith("--"), `${option} requires a value`);
|
|
84
|
+
if (option === "--project") project = path.resolve(value); else options[values[option]] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.log(JSON.stringify(recoverSession(project, options), null, 2));
|
|
88
|
+
} catch (error) { console.error(`NCCGS session recovery refused: ${error.message}`); process.exitCode = 1; }
|
|
89
|
+
}
|
package/scripts/run.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { autonomyPolicy } from "../.claude/nccgs/tools/autonomy.mjs";
|
|
5
5
|
import { containedPath } from "../.claude/nccgs/tools/paths.mjs";
|
|
6
6
|
import { supervise } from "./supervise.mjs";
|
|
7
|
+
import { acquireInstallationLease, assertInstallationAvailable } from "../.claude/nccgs/tools/installation-guard.mjs";
|
|
7
8
|
|
|
8
9
|
// No shell construction, restrictive tool allowlist, or model override.
|
|
9
10
|
export function launchPlan(argv, env = process.env) {
|
|
@@ -16,18 +17,20 @@ export function launchPlan(argv, env = process.env) {
|
|
|
16
17
|
const option = options[i];
|
|
17
18
|
if (option === "--dry-run") { dryRun = true; continue; }
|
|
18
19
|
if (option === "--supervise") { supervised = true; continue; }
|
|
19
|
-
if (!["--project", "--claude", "--task", "--prompt-file", "--max-resumes", "--timeout-ms", "--session-budget-usd", "--recover-run"].includes(option) || !options[i + 1] || options[i + 1].startsWith("--")) throw new Error(`Invalid run option: ${option}; put Claude arguments after --`);
|
|
20
|
+
if (!["--project", "--claude", "--task", "--prompt-file", "--max-resumes", "--timeout-ms", "--session-budget-usd", "--recover-run", "--budget-phase"].includes(option) || !options[i + 1] || options[i + 1].startsWith("--")) throw new Error(`Invalid run option: ${option}; put Claude arguments after --`);
|
|
20
21
|
const value = options[++i];
|
|
21
22
|
if (option === "--project") project = path.resolve(value);
|
|
22
23
|
else if (option === "--claude") binary = value;
|
|
23
24
|
else if (option === "--task") supervision.taskId = value;
|
|
24
25
|
else if (option === "--prompt-file") supervision.promptFile = path.resolve(value);
|
|
25
26
|
else if (option === "--recover-run") { supervision.recoverRun = value; supervised = true; }
|
|
27
|
+
else if (option === "--budget-phase") { if (!["work", "final-review"].includes(value)) throw new Error("Budget phase must be work or final-review"); supervision.budgetPhase = value; }
|
|
26
28
|
else if (option === "--max-resumes") supervision.maxResumes = Number(value);
|
|
27
29
|
else if (option === "--timeout-ms") supervision.timeoutMs = Number(value);
|
|
28
30
|
else supervision.budgetUsd = Number(value);
|
|
29
31
|
}
|
|
30
32
|
project = fs.realpathSync(project);
|
|
33
|
+
assertInstallationAvailable(project);
|
|
31
34
|
if (!fs.existsSync(containedPath(project, ".claude/nccgs/install-state.json"))) throw new Error("Install/update NCCGS in the target project first");
|
|
32
35
|
const mode = autonomyPolicy(project).mode;
|
|
33
36
|
if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) throw new Error("Start nccgs run from the external terminal/operator, not by nesting it inside an active Claude session");
|
|
@@ -35,14 +38,14 @@ export function launchPlan(argv, env = process.env) {
|
|
|
35
38
|
for (const argument of forwarded) if (conflicting.test(argument)) throw new Error(`NCCGS owns session permission/root settings; conflicting option: ${argument}`);
|
|
36
39
|
if (supervised) {
|
|
37
40
|
if (supervision.recoverRun) {
|
|
38
|
-
if (forwarded.length || supervision.taskId || supervision.promptFile || supervision.budgetUsd !== undefined || options.includes("--max-resumes") || options.includes("--timeout-ms")) throw new Error("Recovery preserves the original prompt, limits and provider arguments");
|
|
41
|
+
if (forwarded.length || supervision.taskId || supervision.promptFile || supervision.budgetUsd !== undefined || supervision.budgetPhase || options.includes("--max-resumes") || options.includes("--timeout-ms")) throw new Error("Recovery preserves the original prompt, limits and provider arguments");
|
|
39
42
|
} else {
|
|
40
43
|
if (!supervision.taskId || !supervision.promptFile || !Number.isFinite(supervision.budgetUsd) || supervision.budgetUsd <= 0) throw new Error("Supervision needs --task, --prompt-file and --session-budget-usd");
|
|
41
44
|
if (!Number.isSafeInteger(supervision.maxResumes) || supervision.maxResumes < 0 || supervision.maxResumes > 5 || !Number.isSafeInteger(supervision.timeoutMs) || supervision.timeoutMs < 1000 || supervision.timeoutMs > 86400000) throw new Error("Invalid supervision continuation/time limits");
|
|
42
45
|
const owned = /^(?:-p|--print|--output-format|--input-format|--session-id|--resume|-r|--continue|-c|--fork-session|--no-session-persistence|--max-budget-usd|--verbose|--bg|--background|--remote|--cloud)(?:=|$)/;
|
|
43
46
|
for (const argument of forwarded) if (owned.test(argument)) throw new Error(`Supervisor owns native lifecycle option: ${argument}`);
|
|
44
47
|
}
|
|
45
|
-
} else if (options.some(option => ["--task", "--prompt-file", "--max-resumes", "--timeout-ms", "--session-budget-usd"].includes(option))) throw new Error("Lifecycle options require --supervise");
|
|
48
|
+
} else if (options.some(option => ["--task", "--prompt-file", "--max-resumes", "--timeout-ms", "--session-budget-usd", "--budget-phase"].includes(option))) throw new Error("Lifecycle options require --supervise");
|
|
46
49
|
return { project, binary, dryRun, mode, args: ["--permission-mode", mode === "full" ? "bypassPermissions" : "default", ...forwarded],
|
|
47
50
|
supervision: supervised ? supervision : null, env: { ...env, NCCGS_PROJECT_ROOT: project, NCCGS_TASK_AUTONOMY: mode,
|
|
48
51
|
// Native auto-backgrounding can otherwise turn a claimed foreground worker
|
|
@@ -59,14 +62,24 @@ export async function run(argv) {
|
|
|
59
62
|
return 0;
|
|
60
63
|
}
|
|
61
64
|
console.error(`NCCGS ${plan.mode} task session | ${plan.project} | ${plan.mode === "full" ? "Native task permissions enabled, including outside-project commands; explicit deny rules and integrity guards remain effective." : "Existing guarded permissions retained."}`);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const interrupt = () => child
|
|
65
|
-
process.on("SIGINT", interrupt);
|
|
65
|
+
const lease = acquireInstallationLease(plan.project, "nccgs-run");
|
|
66
|
+
let child = null;
|
|
67
|
+
const interrupt = () => child?.kill("SIGINT");
|
|
66
68
|
try {
|
|
69
|
+
// Recheck after registering the session: uninstall checks this lease before
|
|
70
|
+
// removing any file. A racing launch cannot proceed through both gates.
|
|
71
|
+
assertInstallationAvailable(plan.project);
|
|
72
|
+
// Removal may have finished between launchPlan and lease acquisition.
|
|
73
|
+
if (!fs.existsSync(containedPath(plan.project, ".claude/nccgs/install-state.json")))
|
|
74
|
+
throw new Error("Install/update NCCGS in the target project first; it was removed before launch");
|
|
75
|
+
if (plan.supervision) return await supervise(plan);
|
|
76
|
+
lease.update({ phase: "spawning" });
|
|
77
|
+
child = spawn(plan.binary, plan.args, { cwd: plan.project, env: plan.env, stdio: "inherit", windowsHide: true });
|
|
78
|
+
child.once("spawn", () => lease.update({ phase: "running", child: { pid: child.pid, group: false } }));
|
|
79
|
+
process.on("SIGINT", interrupt);
|
|
67
80
|
return await new Promise((resolve, reject) => {
|
|
68
81
|
child.once("error", error => reject(new Error(`Cannot start Claude Code: ${error.message}. Use --claude with the native executable path.`)));
|
|
69
82
|
child.once("close", (code, signal) => resolve(code ?? (signal ? 130 : 1)));
|
|
70
83
|
});
|
|
71
|
-
} finally { process.off("SIGINT", interrupt); }
|
|
84
|
+
} finally { process.off("SIGINT", interrupt); lease.release(); }
|
|
72
85
|
}
|
package/scripts/supervise.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { taskPath } from "../.claude/nccgs/tools/gates.mjs";
|
|
|
7
7
|
import { checkpoint, status as dispatchStatus } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
8
8
|
import { listJobs } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
9
9
|
import { acquireLock, delay, pidState } from "../.claude/nccgs/tools/process-lock.mjs";
|
|
10
|
+
import { sessionBudgetLock, sessionBudgetCoverage } from "../.claude/nccgs/tools/session-budget.mjs";
|
|
11
|
+
import { allocateReviewBudget } from "../.claude/nccgs/tools/review-budget.mjs";
|
|
12
|
+
import { settleWorkHandoff } from "./work-handoff.mjs";
|
|
10
13
|
|
|
11
14
|
const need = (condition, message) => { if (!condition) throw new Error(message); };
|
|
12
15
|
const now = () => new Date().toISOString();
|
|
@@ -44,11 +47,13 @@ export async function supervise(plan) {
|
|
|
44
47
|
budgetUsd: config.budgetUsd, cumulativeCostUsd: 0, costCoverage: "complete", invocations: [], status: "ready" };
|
|
45
48
|
}
|
|
46
49
|
const lock = acquireLock(contained(project, `.nccgs/runs/task-${state.taskId}.lock`), { runId: state.id });
|
|
50
|
+
let budgetLock;
|
|
47
51
|
const directory = path.dirname(file), save = () => atomicJson(file, state);
|
|
48
52
|
let interrupted = false, child;
|
|
49
53
|
const interrupt = () => { interrupted = true; if (child) child.kill("SIGINT"); };
|
|
50
54
|
process.on("SIGINT", interrupt);
|
|
51
55
|
try {
|
|
56
|
+
budgetLock = acquireLock(contained(project, sessionBudgetLock), { runId: state.id, sessionId: state.sessionId });
|
|
52
57
|
fs.mkdirSync(directory, { recursive: true });
|
|
53
58
|
const task = json(taskPath(project, state.taskId));
|
|
54
59
|
need(task.contractHash === state.contractHash, "Task contract changed during supervised run");
|
|
@@ -63,7 +68,9 @@ export async function supervise(plan) {
|
|
|
63
68
|
state.cumulativeCostUsd = cumulativeCost(state.cumulativeCostUsd, terminal, state.sessionId);
|
|
64
69
|
state.costCoverage = "complete";
|
|
65
70
|
Object.assign(last, { status: "recovered", terminal, recoveredAt: now() }); save();
|
|
71
|
+
last.evidence = artifact(project, `.nccgs/runs/${state.id}/${last.log}`); save();
|
|
66
72
|
}
|
|
73
|
+
allocateReviewBudget(project, state, state.budgetPhase ?? config.budgetPhase ?? "work"); save();
|
|
67
74
|
for (;;) {
|
|
68
75
|
need(!interrupted, "Supervised run interrupted; jobs and checkpoint retained");
|
|
69
76
|
need(Date.now() < state.deadline, "Supervised run deadline reached; no automatic extension");
|
|
@@ -75,11 +82,16 @@ export async function supervise(plan) {
|
|
|
75
82
|
await delay(250); continue; // deterministic wait, no model request or fake verification receipt
|
|
76
83
|
}
|
|
77
84
|
if (state.invocations.length) {
|
|
78
|
-
|
|
85
|
+
let latestTask = json(taskPath(project, state.taskId));
|
|
79
86
|
const latestReceiptAt = Math.max(0, ...jobs.map(j => Date.parse(json(contained(project, j.evidence.path)).finishedAt)));
|
|
80
|
-
const reportAt = Date.parse(latestTask.stages.report?.recordedAt ?? "");
|
|
81
87
|
const active = dispatchStatus(project, state.taskId).active;
|
|
82
88
|
need(!active.length, "Native worker dispatch remains active/uncertain; preserve quota and request reconciliation");
|
|
89
|
+
const handoff = settleWorkHandoff(project, state, latestReceiptAt);
|
|
90
|
+
if (handoff) {
|
|
91
|
+
latestTask = json(taskPath(project, state.taskId));
|
|
92
|
+
state.workHandoff = { origin: "supervisor-work-handoff", evidence: latestTask.stages.report.evidence, recordedAt: latestTask.stages.report.recordedAt };
|
|
93
|
+
}
|
|
94
|
+
const reportAt = Date.parse(latestTask.stages.report?.recordedAt ?? "");
|
|
83
95
|
if (Number.isFinite(reportAt) && reportAt >= latestReceiptAt && reportAt >= Date.parse(state.createdAt)) {
|
|
84
96
|
state.status = "settled"; state.taskOutcome = latestTask.outcome; state.taskPhase = latestTask.phase;
|
|
85
97
|
state.finishedAt = now(); save(); return 0;
|
|
@@ -88,28 +100,40 @@ export async function supervise(plan) {
|
|
|
88
100
|
}
|
|
89
101
|
const remaining = state.budgetUsd - state.cumulativeCostUsd;
|
|
90
102
|
need(remaining > 0, "Session list-cost budget exhausted; no budget reset on resume");
|
|
103
|
+
allocateReviewBudget(project, state, state.budgetPhase ?? "work");
|
|
104
|
+
for (let id = state.taskId; id; id = json(taskPath(project, id)).parentId) {
|
|
105
|
+
const ledger = dispatchStatus(project, id);
|
|
106
|
+
if (ledger.limits?.maxCostUsd != null)
|
|
107
|
+
sessionBudgetCoverage(project, id, ledger.reservations, ledger.limits.maxCostUsd, state, { running: false });
|
|
108
|
+
}
|
|
91
109
|
const index = state.invocations.length, invocation = { index, startedAt: now(), status: "running", pid: null,
|
|
92
110
|
log: `native-${index}.jsonl`, stderr: `native-${index}.stderr.log`, allowanceUsd: remaining };
|
|
93
|
-
const
|
|
111
|
+
const workNotice = task.mode === "implement"
|
|
112
|
+
? `Final delivery review money/time is protected in a separate final-review phase. Finish the implementation record, then invoke node .claude/nccgs/tools/task.mjs report --id ${state.taskId} --outcome partial --summary "Implementation recorded; awaiting protected independent review". A prose PARTIAL response alone does not record the handoff. Do not spend this work phase on delivery review.`
|
|
113
|
+
: "Complete this task's normal analysis, independent design review, report and closure gates. Final delivery review money/time is protected for the later implementation task.";
|
|
114
|
+
const phaseNotice = state.budgetAllocation ? `Budget phase: ${state.budgetPhase}. Effective cumulative session ceiling USD ${state.budgetUsd}; absolute deadline ${new Date(state.deadline).toISOString()}. Read task budget-status for the pinned allocation. ${state.budgetPhase === "work" ? workNotice : "This phase only admits independent delivery review and settlement. No source workers, repair or new verification jobs. Read existing current measured evidence; preserve all ordinary gates and the exact reviewer verdict."}\n\n` : "";
|
|
115
|
+
const prompt = phaseNotice + (index === 0 ? state.prompt : `Continue NCCGS task ${state.taskId} in this same session. Read task inspect and task job-status. Use the existing verification receipts; do not rerun successful commands just to create receipts. Investigate failed checks within remaining scope/budget. Record a truthful report (success/partial/blocked/failed). Do not claim DONE without all configured gates. Jobs: ${JSON.stringify(jobs)}. This continuation preserves all previous attempts, review rounds and costs.`);
|
|
94
116
|
state.invocations.push(invocation); state.status = "running"; state.costCoverage = "unknown-current-invocation"; save();
|
|
95
117
|
lock.update({ phase: "spawning" });
|
|
118
|
+
budgetLock.update({ phase: "spawning" });
|
|
96
119
|
const output = fs.openSync(path.join(directory, invocation.log), "wx"), errors = fs.openSync(path.join(directory, invocation.stderr), "wx");
|
|
97
120
|
const args = [...state.providerArgs, "-p", "--output-format", "stream-json", "--verbose", "--max-budget-usd", String(remaining),
|
|
98
121
|
...(index === 0 ? ["--session-id", state.sessionId] : ["--resume", state.sessionId])];
|
|
99
122
|
let deadlineTimer;
|
|
100
123
|
try {
|
|
101
124
|
child = spawn(state.binary, args, { cwd: project, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
-
env: { ...plan.env, NCCGS_SUPERVISED_SESSION: state.sessionId, NCCGS_TASK_ID: state.taskId } });
|
|
125
|
+
env: { ...plan.env, NCCGS_SUPERVISED_SESSION: state.sessionId, NCCGS_SUPERVISED_RUN: state.id, NCCGS_TASK_ID: state.taskId } });
|
|
103
126
|
child.stdin.on("error", () => {}); // early native failure is captured in its terminal/exit evidence
|
|
104
127
|
child.stdout.on("data", data => { fs.writeSync(output, data); process.stdout.write(data); });
|
|
105
128
|
child.stderr.on("data", data => { fs.writeSync(errors, data); process.stderr.write(data); });
|
|
106
|
-
child.once("spawn", () => { invocation.pid = child.pid; save();
|
|
129
|
+
child.once("spawn", () => { invocation.pid = child.pid; save(); const ownership = { phase: "running", child: { pid: child.pid, group: false } }; lock.update(ownership); budgetLock.update(ownership); });
|
|
107
130
|
child.stdin.end(prompt);
|
|
108
131
|
deadlineTimer = setTimeout(() => { interrupted = true; child.kill("SIGINT"); }, Math.max(1, state.deadline - Date.now()));
|
|
109
132
|
const result = await new Promise((resolve, reject) => { child.once("error", reject); child.once("close", (code, signal) => resolve({ code, signal })); });
|
|
110
133
|
Object.assign(invocation, result, { finishedAt: now() });
|
|
111
134
|
} finally { clearTimeout(deadlineTimer); fs.closeSync(output); fs.closeSync(errors); child = null; }
|
|
112
135
|
lock.update({ phase: "owned", child: null });
|
|
136
|
+
budgetLock.update({ phase: "owned", child: null });
|
|
113
137
|
invocation.terminal = terminalFrom(path.join(directory, invocation.log));
|
|
114
138
|
state.cumulativeCostUsd = cumulativeCost(state.cumulativeCostUsd, invocation.terminal, state.sessionId);
|
|
115
139
|
state.costCoverage = "complete";
|
|
@@ -120,5 +144,5 @@ export async function supervise(plan) {
|
|
|
120
144
|
state.status = "blocked"; state.reason = error.message; state.stoppedAt = now();
|
|
121
145
|
try { state.checkpoint = checkpoint(project, state.taskId, { reason: error.message }); } catch { /* original failure retained */ }
|
|
122
146
|
save(); console.error(`NCCGS supervisor: ${error.message}. Run record: ${file}`); return 1;
|
|
123
|
-
} finally { process.off("SIGINT", interrupt); lock.release(); }
|
|
147
|
+
} finally { process.off("SIGINT", interrupt); budgetLock?.release(); lock.release(); }
|
|
124
148
|
}
|