nccgs 1.3.0 → 2.0.0-rc.2
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/agents/nccgs-adversarial-reviewer.md +17 -6
- package/.claude/agents/nccgs-engine-programmer.md +2 -2
- package/.claude/agents/nccgs-fast-implementer.md +30 -0
- package/.claude/agents/nccgs-game-designer.md +26 -0
- package/.claude/agents/nccgs-gameplay-programmer.md +3 -4
- package/.claude/agents/nccgs-qa-engineer.md +2 -2
- package/.claude/agents/nccgs-task-scout.md +24 -0
- package/.claude/agents/nccgs-ui-programmer.md +2 -2
- package/.claude/agents/nccgs-unity-build-specialist.md +2 -2
- package/.claude/agents/nccgs-unity-implementer.md +7 -6
- package/.claude/agents/nccgs-unity-systems-specialist.md +2 -2
- package/.claude/agents/nccgs-unity-ui-specialist.md +2 -2
- package/.claude/agents/nccgs-verification-engineer.md +2 -2
- package/.claude/nccgs/MEMORY.md +59 -0
- package/.claude/nccgs/THIRD_PARTY_NOTICES.md +16 -0
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/agent-library/nccgs-adversarial-reviewer.md +16 -5
- package/.claude/nccgs/agent-library/nccgs-fast-implementer.md +28 -0
- package/.claude/nccgs/agent-library/nccgs-game-designer.md +1 -2
- package/.claude/nccgs/agent-library/nccgs-gameplay-programmer.md +1 -2
- package/.claude/nccgs/agent-library/nccgs-task-scout.md +22 -0
- package/.claude/nccgs/agent-library/nccgs-unity-implementer.md +5 -4
- package/.claude/nccgs/constitution.md +44 -12
- package/.claude/nccgs/hooks/agent-audit.mjs +2 -2
- package/.claude/nccgs/hooks/closure-guard.mjs +31 -8
- package/.claude/nccgs/hooks/common.mjs +2 -1
- package/.claude/nccgs/hooks/runtime-observation.mjs +2 -2
- package/.claude/nccgs/hooks/session-start.mjs +11 -4
- package/.claude/nccgs/packages/ponytail/LICENSE +21 -0
- package/.claude/nccgs/packages/ponytail/NOTICE.md +32 -0
- package/.claude/nccgs/packages/ponytail/README.md +46 -0
- package/.claude/nccgs/packages/ponytail/complexity-review.md +47 -0
- package/.claude/nccgs/packages/ponytail/engineering.md +68 -0
- package/.claude/nccgs/packages/ponytail/manifest.json +24 -0
- package/.claude/nccgs/pipeline.json +92 -8
- package/.claude/nccgs/protocols/agent-contract.md +50 -1
- package/.claude/nccgs/protocols/astra-claude.md +46 -12
- package/.claude/nccgs/protocols/bounded-repair.md +118 -0
- package/.claude/nccgs/protocols/context-packets.md +5 -0
- package/.claude/nccgs/protocols/delivery-acceptance.md +105 -0
- package/.claude/nccgs/protocols/design-lifecycle.md +139 -0
- package/.claude/nccgs/protocols/evidence.md +11 -1
- package/.claude/nccgs/protocols/model-routing.md +44 -42
- package/.claude/nccgs/protocols/orchestration.md +46 -5
- package/.claude/nccgs/protocols/review-handoff.md +65 -0
- package/.claude/nccgs/protocols/standalone-gates.md +273 -0
- package/.claude/nccgs/protocols/standalone-pilot.md +144 -0
- package/.claude/nccgs/protocols/standalone.md +187 -0
- package/.claude/nccgs/protocols/task-autonomy.md +55 -0
- package/.claude/nccgs/protocols/task-gates.md +73 -7
- package/.claude/nccgs/routing.generated.md +25 -7
- package/.claude/nccgs/settings.fragment.json +153 -141
- package/.claude/nccgs/studio.json +135 -101
- package/.claude/nccgs/tools/autonomy.mjs +27 -0
- package/.claude/nccgs/tools/command-process.mjs +44 -0
- package/.claude/nccgs/tools/compile-policy.mjs +34 -4
- package/.claude/nccgs/tools/configure-models.mjs +13 -5
- package/.claude/nccgs/tools/criteria.mjs +121 -0
- package/.claude/nccgs/tools/delivery-budget.mjs +52 -0
- package/.claude/nccgs/tools/design-readiness.mjs +158 -0
- package/.claude/nccgs/tools/evidence.mjs +153 -0
- package/.claude/nccgs/tools/gates.mjs +151 -24
- package/.claude/nccgs/tools/inputs.mjs +73 -0
- package/.claude/nccgs/tools/lifecycle.mjs +521 -0
- package/.claude/nccgs/tools/native-completion.mjs +21 -0
- package/.claude/nccgs/tools/orchestration.mjs +430 -0
- package/.claude/nccgs/tools/paths.mjs +60 -0
- package/.claude/nccgs/tools/policy.mjs +58 -13
- package/.claude/nccgs/tools/process-lock.mjs +74 -0
- package/.claude/nccgs/tools/repair-packet.mjs +87 -0
- package/.claude/nccgs/tools/review-handoff.mjs +32 -0
- package/.claude/nccgs/tools/runtime.mjs +33 -7
- package/.claude/nccgs/tools/task.mjs +62 -31
- package/.claude/nccgs/tools/tool-guard.mjs +117 -0
- package/.claude/nccgs/tools/verification-jobs.mjs +161 -0
- package/.claude/nccgs/tools/verification-worker.mjs +71 -0
- package/.claude/nccgs/tools/windows-job.cs +96 -0
- package/.claude/nccgs/workflow-catalog.json +286 -26
- package/.claude/skills/accessibility-review/SKILL.md +12 -5
- package/.claude/skills/architecture-decision/SKILL.md +12 -5
- package/.claude/skills/asset-audit/SKILL.md +12 -5
- package/.claude/skills/audit/SKILL.md +12 -5
- package/.claude/skills/balance-review/SKILL.md +12 -5
- package/.claude/skills/bug-triage/SKILL.md +12 -5
- package/.claude/skills/bug-triage/references/astra.md +12 -5
- package/.claude/skills/closure/SKILL.md +12 -5
- package/.claude/skills/compatibility-review/SKILL.md +12 -5
- package/.claude/skills/context-pack/SKILL.md +12 -5
- package/.claude/skills/dependency-review/SKILL.md +12 -5
- package/.claude/skills/design/SKILL.md +14 -5
- package/.claude/skills/design-review/SKILL.md +14 -5
- package/.claude/skills/evidence-review/SKILL.md +12 -5
- package/.claude/skills/hotfix/SKILL.md +12 -5
- package/.claude/skills/hotfix/references/astra.md +12 -5
- package/.claude/skills/incident-recovery/SKILL.md +12 -5
- package/.claude/skills/incident-recovery/references/astra.md +12 -5
- package/.claude/skills/localize-game/SKILL.md +12 -5
- package/.claude/skills/localize-game/references/astra.md +12 -5
- package/.claude/skills/migrate-project/SKILL.md +12 -5
- package/.claude/skills/migrate-project/references/astra.md +12 -5
- package/.claude/skills/migrate-project/references/procedure.md +2 -1
- package/.claude/skills/milestone-review/SKILL.md +12 -5
- package/.claude/skills/nccgs-code-review/SKILL.md +12 -5
- package/.claude/skills/nccgs-code-review/references/astra.md +8 -2
- package/.claude/skills/performance-audit/SKILL.md +12 -5
- package/.claude/skills/plan-feature/SKILL.md +12 -5
- package/.claude/skills/playtest/SKILL.md +12 -5
- package/.claude/skills/playtest/references/astra.md +16 -5
- package/.claude/skills/project-stage/SKILL.md +12 -5
- package/.claude/skills/prototype-feature/SKILL.md +12 -5
- package/.claude/skills/prototype-feature/references/astra.md +16 -5
- package/.claude/skills/qa-plan/SKILL.md +12 -5
- package/.claude/skills/release/SKILL.md +12 -5
- package/.claude/skills/release/references/astra.md +12 -5
- package/.claude/skills/release-readiness/SKILL.md +12 -5
- package/.claude/skills/retrospective/SKILL.md +12 -5
- package/.claude/skills/review/SKILL.md +12 -5
- package/.claude/skills/review/references/astra.md +8 -2
- package/.claude/skills/security-audit/SKILL.md +12 -5
- package/.claude/skills/sprint-plan/SKILL.md +12 -5
- package/.claude/skills/status/SKILL.md +12 -5
- package/.claude/skills/status/references/astra.md +6 -0
- package/.claude/skills/story-readiness/SKILL.md +14 -5
- package/.claude/skills/test/SKILL.md +12 -5
- package/.claude/skills/test/references/astra.md +12 -5
- package/.claude/skills/ui-review/SKILL.md +12 -5
- package/.claude/skills/work/SKILL.md +12 -5
- package/.claude/skills/work/references/astra.md +17 -5
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +2 -0
- package/MEMORY.md +55 -0
- package/README.md +132 -180
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/UPGRADING.md +91 -69
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +19 -5
- package/docs/BOUNDED-REPAIR.md +11 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +46 -34
- package/docs/NCCGS-1.3.md +10 -122
- package/docs/NCCGS-1.4.1.md +149 -0
- package/docs/NCCGS-1.4.md +184 -0
- package/docs/NCCGS-1.5-IMPLEMENTATION-PLAN.md +378 -0
- package/docs/NCCGS-1.5-PILOT.md +130 -0
- package/docs/NCCGS-1.5.md +237 -0
- package/docs/PHASE-1-OPERATIONS.md +137 -0
- package/docs/PHASE-2-OPERATIONS.md +183 -0
- package/docs/PROJECT-POLICY.md +21 -9
- package/docs/RELEASE-STATUS.md +42 -0
- package/docs/WORKFLOWS.md +42 -24
- package/package.json +7 -3
- package/scaffold/.nccgs/autonomy.json +4 -0
- package/scaffold/.nccgs/execution.json +10 -0
- package/scaffold/.nccgs/inputs.json +6 -0
- package/scaffold/.nccgs/project.yaml +4 -2
- package/scaffold/.nccgs/templates/agent-handoff.md +2 -0
- package/scaffold/.nccgs/templates/design-contract.json +34 -0
- package/scaffold/.nccgs/templates/feature-contract.md +21 -5
- package/scaffold/.nccgs/templates/implementation-report.md +4 -1
- package/scaffold/.nccgs/templates/repair-packet.json +14 -0
- package/scripts/benchmark-snapshot.mjs +205 -0
- package/scripts/benchmark-v15.mjs +173 -0
- package/scripts/cli.mjs +129 -2
- package/scripts/doctor.mjs +128 -22
- package/scripts/install.mjs +96 -34
- package/scripts/run.mjs +72 -0
- package/scripts/supervise.mjs +124 -0
- package/scripts/validate.mjs +45 -4
- package/tests/autonomy.test.mjs +113 -0
- package/tests/benchmark-v15.test.mjs +65 -0
- package/tests/design-readiness.test.mjs +200 -0
- package/tests/evidence.test.mjs +186 -0
- package/tests/framework.test.mjs +333 -16
- package/tests/gates.test.mjs +313 -4
- package/tests/lifecycle-v15.test.mjs +300 -0
- package/tests/orchestration-v15.test.mjs +545 -0
- package/tests/phase1.test.mjs +324 -0
- package/tests/phase2.test.mjs +250 -0
- package/tests/policy-v15.test.mjs +220 -0
- package/tests/ponytail-package.test.mjs +75 -0
- package/tests/runtime-v15.test.mjs +624 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { atomicJson, hash, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
+
import { initialize, record, snapshot, taskPath } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
+
import { reserve, reconcile, status } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const label = "SYNTHETIC FRAMEWORK BENCHMARK — no provider/model or game execution";
|
|
13
|
+
export const scenarios = [
|
|
14
|
+
{ id: "tiny-fix", name: "Tiny local fix", mode: "implement", scope: "local", target: "fix", expectedRisk: "FAST", objective: "Fix a local display label alignment with a known expected value" },
|
|
15
|
+
{ id: "learning-prototype", name: "Learning prototype", mode: "implement", scope: "feature", target: "learning-prototype", expectedRisk: "STANDARD", objective: "Explore a bounded feedback hypothesis in a synthetic prototype" },
|
|
16
|
+
{ id: "alpha-ready-prototype", name: "Alpha-ready prototype", mode: "implement", scope: "feature", target: "alpha-ready", expectedRisk: "STANDARD", objective: "Prepare a prototype feature for a declared end-to-end readiness check" },
|
|
17
|
+
{ id: "cross-system-design", name: "Cross-system optimization and design plan", mode: "plan", scope: "cross-system", target: "production-change", expectedRisk: "CONTROLLED", objective: "Plan cross-system allocation and update-order optimization from a measured baseline" },
|
|
18
|
+
{ id: "large-system-audit", name: "Large feature and system audit", mode: "audit", scope: "cross-system", target: "production-change", expectedRisk: "CONTROLLED", objective: "Audit dependencies and acceptance coverage across a large feature's systems" }
|
|
19
|
+
];
|
|
20
|
+
const measure = action => {
|
|
21
|
+
const start = performance.now(), value = action();
|
|
22
|
+
return { value, ms: performance.now() - start };
|
|
23
|
+
};
|
|
24
|
+
const rounded = value => Math.round(value * 1000) / 1000;
|
|
25
|
+
function distribution(values) {
|
|
26
|
+
const sorted = [...values].sort((a, b) => a - b), mid = Math.floor(sorted.length / 2);
|
|
27
|
+
return { samples: sorted.length, min: rounded(sorted[0]), median: rounded(sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2), max: rounded(sorted.at(-1)), total: rounded(sorted.reduce((a, b) => a + b, 0)) };
|
|
28
|
+
}
|
|
29
|
+
function workspace(sourceFiles) {
|
|
30
|
+
const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-v15-benchmark-"));
|
|
31
|
+
function write(relative, value) {
|
|
32
|
+
const file = path.join(project, relative);
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
fs.writeFileSync(file, typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file), "utf8"));
|
|
37
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
38
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" }, acceptanceRequired: false, requireObservedEffort: false, maxParallelWorkers: 2 });
|
|
39
|
+
for (let i = 0; i < sourceFiles; i++) write(`SyntheticSource/component-${i}.txt`, `Synthetic framework fixture component ${i}.\n${"Boundary and dependency data only.\n".repeat(24)}`);
|
|
40
|
+
return { project, write,
|
|
41
|
+
init(id, scenario, parentId = null) {
|
|
42
|
+
const relative = `.nccgs/intakes/${id}.json`;
|
|
43
|
+
write(relative, { schemaVersion: 1, objective: scenario.objective, mode: scenario.mode, scope: scenario.scope, target: scenario.target, readiness: "ready", systems: ["synthetic-component"], requiredChecks: ["fixture-acceptance"] });
|
|
44
|
+
return initialize(project, id, null, null, null, { intake: relative, parentId });
|
|
45
|
+
},
|
|
46
|
+
dispose() {
|
|
47
|
+
const resolved = path.resolve(project), expectedParent = path.resolve(os.tmpdir());
|
|
48
|
+
assert.equal(path.dirname(resolved), expectedParent);
|
|
49
|
+
assert.ok(path.basename(resolved).startsWith("nccgs-v15-benchmark-"));
|
|
50
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
function request(task, key, maxDispatches = null) {
|
|
54
|
+
// The stress parameter counts ordinary delivery attempts; protected slots stay
|
|
55
|
+
// inside the actual total ceiling and must remain untouched by this fixture.
|
|
56
|
+
const protectedSlots = Object.values(task.contract.deliveryReserve ?? {}).reduce((a, b) => a + b, 0);
|
|
57
|
+
return { kind: task.mode === "implement" ? "implementation" : "analysis", agent: task.mode === "implement" ? "nccgs-gameplay-programmer" : "nccgs-task-scout",
|
|
58
|
+
requestKey: key, reason: "Synthetic reservation only; no worker/provider is invoked", writePaths: task.mode === "implement" ? ["SyntheticSource"] : [],
|
|
59
|
+
...(maxDispatches === null ? {} : { limits: { maxDispatches: maxDispatches + protectedSlots } }) };
|
|
60
|
+
}
|
|
61
|
+
function cancelFixture(project, dispatch) {
|
|
62
|
+
return reconcile(project, dispatch.id, { status: "cancelled", reason: "Synthetic framework reservation completed without dispatching a model", reference: "benchmark-v15: intentionally no provider invocation" });
|
|
63
|
+
}
|
|
64
|
+
function scenarioRun(scenario, iterations, sourceFiles) {
|
|
65
|
+
const times = { init: [], snapshot: [], reserve: [], report: [] }, counts = { initialized: 0, snapshots: 0, reservations: 0, reports: 0, providerAttempts: 0 };
|
|
66
|
+
let source = null, observed = null;
|
|
67
|
+
for (let i = 0; i < iterations; i++) {
|
|
68
|
+
const fixture = workspace(sourceFiles);
|
|
69
|
+
try {
|
|
70
|
+
const initialized = measure(() => fixture.init(scenario.id, scenario));
|
|
71
|
+
const task = initialized.value;
|
|
72
|
+
assert.equal(task.mode, scenario.mode); assert.equal(task.risk, scenario.expectedRisk);
|
|
73
|
+
assert.equal(task.schemaVersion, 2);
|
|
74
|
+
const expectedRoute = { FAST: { class: "lightweight", model: "sonnet", effort: "medium" }, STANDARD: { class: "standard", model: "opus", effort: "medium" }, CONTROLLED: { class: "deep", model: "opus", effort: "high" } }[scenario.expectedRisk];
|
|
75
|
+
assert.deepEqual(task.implementationRoute, expectedRoute);
|
|
76
|
+
observed = { mode: task.mode, risk: task.risk, implementationRoute: task.implementationRoute };
|
|
77
|
+
times.init.push(initialized.ms); counts.initialized++;
|
|
78
|
+
const captured = measure(() => snapshot(fixture.project, task));
|
|
79
|
+
times.snapshot.push(captured.ms); counts.snapshots++;
|
|
80
|
+
source = { syntheticFiles: sourceFiles, snapshotFiles: captured.value.files, snapshotBytes: captured.value.bytes };
|
|
81
|
+
const reserved = measure(() => reserve(fixture.project, task.id, request(task, "measurement")));
|
|
82
|
+
times.reserve.push(reserved.ms); counts.reservations++;
|
|
83
|
+
cancelFixture(fixture.project, reserved.value);
|
|
84
|
+
const reported = measure(() => record(fixture.project, task.id, "report", { outcome: "blocked", summary: `${label}. Classification, snapshot, reservation and reporting were exercised; there is no runtime/model evidence.` }));
|
|
85
|
+
times.report.push(reported.ms); counts.reports++;
|
|
86
|
+
const usage = status(fixture.project, task.id);
|
|
87
|
+
assert.equal(usage.usage.dispatches, 1);
|
|
88
|
+
assert.equal(usage.telemetry.attempts, 0);
|
|
89
|
+
assert.equal(reported.value.reviewBudget.used, 0);
|
|
90
|
+
assert.equal(reported.value.stages.report.verificationState.verified, false);
|
|
91
|
+
assert.equal(reported.value.outcome, "blocked");
|
|
92
|
+
} finally { fixture.dispose(); }
|
|
93
|
+
}
|
|
94
|
+
return { id: scenario.id, name: scenario.name, expected: { mode: scenario.mode, risk: scenario.expectedRisk }, observed, result: "PASS",
|
|
95
|
+
counts, source, timingsMs: Object.fromEntries(Object.entries(times).map(([name, values]) => [name, distribution(values)])) };
|
|
96
|
+
}
|
|
97
|
+
function stressQuota(sourceFiles, quota) {
|
|
98
|
+
const fixture = workspace(sourceFiles), start = performance.now();
|
|
99
|
+
try {
|
|
100
|
+
const task = fixture.init("bounded", scenarios[0]);
|
|
101
|
+
for (let i = 0; i < quota; i++) cancelFixture(fixture.project, reserve(fixture.project, task.id, request(task, `try-${i}`, quota)));
|
|
102
|
+
assert.throws(() => reserve(fixture.project, task.id, request(task, "over-budget", quota)), /budget reserved for bounded/i);
|
|
103
|
+
const after = status(fixture.project, task.id);
|
|
104
|
+
assert.equal(after.usage.dispatches, quota);
|
|
105
|
+
assert.equal(after.telemetry.attempts, 0);
|
|
106
|
+
assert.equal(after.active.length, 0);
|
|
107
|
+
assert.equal(after.deliveryBudget.protectedSlots, 4);
|
|
108
|
+
return { id: "repeated-reservation-exhaustion", result: "PASS", quota, reservations: after.usage.dispatches, rejectedAdditionalReservation: true,
|
|
109
|
+
quotaKind: "ordinary-delivery", totalDispatchLimit: after.limits.maxDispatches, protectedRemaining: after.deliveryBudget.remaining,
|
|
110
|
+
cancelledAttemptsRefunded: false, providerAttempts: 0, durationMs: rounded(performance.now() - start) };
|
|
111
|
+
} finally { fixture.dispose(); }
|
|
112
|
+
}
|
|
113
|
+
function stressParent(sourceFiles) {
|
|
114
|
+
const fixture = workspace(sourceFiles), start = performance.now();
|
|
115
|
+
try {
|
|
116
|
+
const parent = fixture.init("parent", scenarios[1]);
|
|
117
|
+
cancelFixture(fixture.project, reserve(fixture.project, parent.id, request(parent, "parent-integration", 4)));
|
|
118
|
+
const first = fixture.init("child-a", scenarios[0], parent.id);
|
|
119
|
+
for (let i = 0; i < 2; i++) cancelFixture(fixture.project, reserve(fixture.project, first.id, request(first, `child-a-${i}`, 3)));
|
|
120
|
+
const second = fixture.init("child-b", scenarios[0], parent.id);
|
|
121
|
+
cancelFixture(fixture.project, reserve(fixture.project, second.id, request(second, "child-b-0", 3)));
|
|
122
|
+
assert.throws(() => reserve(fixture.project, second.id, request(second, "child-b-over-parent", 3)), /budget reserved for parent/i);
|
|
123
|
+
const third = fixture.init("child-c", scenarios[0], parent.id);
|
|
124
|
+
assert.throws(() => reserve(fixture.project, third.id, request(third, "new-child-no-refund", 3)), /budget reserved for parent/i);
|
|
125
|
+
const aggregate = status(fixture.project, parent.id);
|
|
126
|
+
assert.equal(aggregate.usage.dispatches, 4);
|
|
127
|
+
assert.equal(aggregate.telemetry.attempts, 0);
|
|
128
|
+
assert.equal(json(taskPath(fixture.project, parent.id)).reviewBudget.used, 0);
|
|
129
|
+
return { id: "parent-child-aggregate-exhaustion", result: "PASS", parentQuota: 4, aggregateReservations: 4,
|
|
130
|
+
quotaKind: "ordinary-delivery", totalDispatchLimit: aggregate.limits.maxDispatches, protectedRemaining: aggregate.deliveryBudget.remaining,
|
|
131
|
+
ownParentReservations: 1, childReservations: { "child-a": 2, "child-b": 1, "child-c": 0 },
|
|
132
|
+
blockedExistingChildDespiteLocalCapacity: true, blockedNewChildDespiteLocalCapacity: true,
|
|
133
|
+
providerAttempts: 0, durationMs: rounded(performance.now() - start) };
|
|
134
|
+
} finally { fixture.dispose(); }
|
|
135
|
+
}
|
|
136
|
+
export function runBenchmark({ iterations = 3, sourceFiles = 40, stressQuota: quota = 6 } = {}) {
|
|
137
|
+
assert(!(process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT), "Run the synthetic framework benchmark outside Claude Code; it uses explicit fixture reconciliation");
|
|
138
|
+
assert(Number.isSafeInteger(iterations) && iterations >= 1 && iterations <= 10, "iterations must be 1..10");
|
|
139
|
+
assert(Number.isSafeInteger(sourceFiles) && sourceFiles >= 1 && sourceFiles <= 1000, "sourceFiles must be 1..1000");
|
|
140
|
+
assert(Number.isSafeInteger(quota) && quota >= 1 && quota <= 32, "stressQuota must be 1..32");
|
|
141
|
+
const start = performance.now();
|
|
142
|
+
const results = scenarios.map(scenario => scenarioRun(scenario, iterations, sourceFiles));
|
|
143
|
+
const stress = [stressQuota(sourceFiles, quota), stressParent(sourceFiles)];
|
|
144
|
+
return { schemaVersion: 1, benchmark: "nccgs-v15-framework-only", label, recordedAt: new Date().toISOString(), result: "PASS",
|
|
145
|
+
environment: { node: process.version, platform: process.platform, architecture: process.arch },
|
|
146
|
+
framework: { packageVersion: json(path.join(root, "package.json")).version, pipelineSha256: hash(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"))) },
|
|
147
|
+
parameters: { iterations, sourceFiles, stressQuota: quota },
|
|
148
|
+
scenarios: results, stress,
|
|
149
|
+
totals: { scenarioRuns: scenarios.length * iterations, stressCases: stress.length, providerAttempts: 0, frameworkDurationMs: rounded(performance.now() - start) },
|
|
150
|
+
evidenceBoundary: { evidenceKind: "synthetic-framework-fixtures", modelExecution: "NOT_RUN", gameExecution: "NOT_RUN", tokenCost: "NOT_MEASURED", productivityComparison: "NOT_MEASURED",
|
|
151
|
+
interpretation: "Timings measure local framework I/O and bookkeeping only; total duration also includes fixture setup and cleanup. Fixture classification and quota assertions do not establish natural-language classification accuracy, model quality or whole-workflow efficiency." } };
|
|
152
|
+
}
|
|
153
|
+
export function run(args) {
|
|
154
|
+
const accepted = new Set(["--out", "--iterations", "--source-files", "--stress-quota"]);
|
|
155
|
+
const options = {};
|
|
156
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
157
|
+
assert(accepted.has(args[i]) && args[i + 1] && !args[i + 1].startsWith("--"), "Usage: benchmark-v15.mjs [--out FILE] [--iterations 1..10] [--source-files 1..1000] [--stress-quota 1..32]");
|
|
158
|
+
assert(options[args[i]] === undefined, `Duplicate option: ${args[i]}`);
|
|
159
|
+
options[args[i]] = args[i + 1];
|
|
160
|
+
}
|
|
161
|
+
const result = runBenchmark({ ...(options["--iterations"] ? { iterations: Number(options["--iterations"]) } : {}),
|
|
162
|
+
...(options["--source-files"] ? { sourceFiles: Number(options["--source-files"]) } : {}), ...(options["--stress-quota"] ? { stressQuota: Number(options["--stress-quota"]) } : {}) });
|
|
163
|
+
if (options["--out"]) {
|
|
164
|
+
const file = path.resolve(options["--out"]);
|
|
165
|
+
atomicJson(file, result);
|
|
166
|
+
return { result: result.result, label, output: file, totals: result.totals };
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
171
|
+
try { console.log(JSON.stringify(run(process.argv.slice(2)), null, 2)); }
|
|
172
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
173
|
+
}
|
package/scripts/cli.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { atomicJson, contained, executionPolicy, hash, json, profilePolicy, resolveExecution } from "../.claude/nccgs/tools/policy.mjs";
|
|
7
9
|
|
|
8
10
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const root = path.resolve(scriptDir, "..");
|
|
@@ -19,6 +21,14 @@ if (command === "version" || command === "--version" || command === "-v") {
|
|
|
19
21
|
process.exit(0);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
if (command === "run") {
|
|
25
|
+
try { process.exitCode = await (await import("./run.mjs")).run(args); }
|
|
26
|
+
catch (error) { console.error(`NCCGS run failed: ${error.message}`); process.exitCode = 1; }
|
|
27
|
+
} else if (command === "migrate-execution") {
|
|
28
|
+
try { migrateExecution(); }
|
|
29
|
+
catch (error) { console.error(`Execution migration failed: ${error.message}`); process.exitCode = 1; }
|
|
30
|
+
} else {
|
|
31
|
+
|
|
22
32
|
if (!new Set(["install", "update", "doctor", "task", "agents", "policy"]).has(command)) {
|
|
23
33
|
console.error(`Unknown command: ${command}`);
|
|
24
34
|
printHelp();
|
|
@@ -39,6 +49,116 @@ if (result.error) {
|
|
|
39
49
|
process.exit(1);
|
|
40
50
|
}
|
|
41
51
|
process.exit(result.status ?? 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function migrateExecution() {
|
|
55
|
+
const value = flag => {
|
|
56
|
+
const index = args.indexOf(flag);
|
|
57
|
+
if (index < 0) return undefined;
|
|
58
|
+
if (!args[index + 1] || args[index + 1].startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
59
|
+
return args[index + 1];
|
|
60
|
+
};
|
|
61
|
+
const target = path.resolve(value("--project") || process.cwd());
|
|
62
|
+
const dryRun = args.includes("--dry-run");
|
|
63
|
+
const rollback = value("--rollback");
|
|
64
|
+
const allowed = new Set(["--project", "--to", "--external-review", "--model-profile", "--rollback", "--dry-run"]);
|
|
65
|
+
for (let i = 0; i < args.length; i++) {
|
|
66
|
+
if (!allowed.has(args[i])) throw new Error(`Unknown migration option: ${args[i]}`);
|
|
67
|
+
if (args[i] !== "--dry-run") i++;
|
|
68
|
+
}
|
|
69
|
+
const at = relative => contained(target, relative);
|
|
70
|
+
if (!fs.existsSync(at(".claude/nccgs/install-state.json"))) throw new Error("Install/update NCCGS before migrating execution");
|
|
71
|
+
const studio = json(at(".claude/nccgs/studio.json"));
|
|
72
|
+
const permitted = new Set([".nccgs/execution.json", ".nccgs/project.yaml", ".claude/settings.json", ".nccgs/agent-activation.json", ".claude/nccgs/install-state.json",
|
|
73
|
+
...studio.agents.map(agent => `.claude/agents/${agent.name}.md`)]);
|
|
74
|
+
const currentHash = relative => fs.existsSync(at(relative)) ? hash(fs.readFileSync(at(relative))) : null;
|
|
75
|
+
const tasks = [];
|
|
76
|
+
const tasksDir = at(".nccgs/tasks");
|
|
77
|
+
if (fs.existsSync(tasksDir)) for (const file of fs.readdirSync(tasksDir).filter(file => file.endsWith(".json"))) {
|
|
78
|
+
const task = json(at(`.nccgs/tasks/${file}`));
|
|
79
|
+
if (!task.closed) tasks.push(task.id || file);
|
|
80
|
+
}
|
|
81
|
+
const reportTasks = () => console.log(`Active tasks (${tasks.length}): ${tasks.join(", ") || "none"}. Existing tasks retain pinned obligations, evidence and consumed budgets; use an explicit continuation/re-pin before applying changed policy. No approval is created.`);
|
|
82
|
+
const restore = (manifest, directory) => {
|
|
83
|
+
for (const item of manifest.files) {
|
|
84
|
+
const destination = at(item.path);
|
|
85
|
+
if (item.before === null) { if (fs.existsSync(destination)) fs.unlinkSync(at(item.path)); }
|
|
86
|
+
else {
|
|
87
|
+
const source = at(`${directory}/files/${item.path}`);
|
|
88
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
89
|
+
fs.copyFileSync(source, at(item.path));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
if (rollback) {
|
|
94
|
+
if (value("--to") || value("--external-review") || value("--model-profile")) throw new Error("Rollback cannot be combined with migration changes");
|
|
95
|
+
if (!/^[0-9a-f-]{36}$/.test(rollback)) throw new Error("Rollback requires the migration UUID printed by this command");
|
|
96
|
+
const directory = `.nccgs/backups/execution/${rollback}`;
|
|
97
|
+
const manifest = json(at(`${directory}/manifest.json`));
|
|
98
|
+
if (manifest.schemaVersion !== 1 || manifest.kind !== "execution-migration" || manifest.id !== rollback || manifest.status !== "applied" || !Array.isArray(manifest.files)) throw new Error("Invalid or already restored execution migration");
|
|
99
|
+
if (new Set(manifest.files.map(item => item.path)).size !== manifest.files.length || manifest.files.length !== permitted.size) throw new Error("Incomplete or duplicate migration configuration records");
|
|
100
|
+
for (const item of manifest.files) {
|
|
101
|
+
if (!permitted.has(item.path)) throw new Error(`Unsafe migration record: ${item.path}`);
|
|
102
|
+
if (currentHash(item.path) !== item.after) throw new Error(`Configuration changed after migration: ${item.path}; rollback would overwrite later work`);
|
|
103
|
+
if (item.before !== null && hash(fs.readFileSync(at(`${directory}/files/${item.path}`))) !== item.before) throw new Error(`Migration backup missing or changed: ${item.path}`);
|
|
104
|
+
}
|
|
105
|
+
console.log(`Restore execution migration ${rollback}; framework version must match the archived configuration. Tasks and evidence are preserved.`);
|
|
106
|
+
reportTasks();
|
|
107
|
+
if (dryRun) { console.log("Dry run complete; no files changed."); return; }
|
|
108
|
+
restore(manifest, directory);
|
|
109
|
+
atomicJson(at(`${directory}/manifest.json`), { ...manifest, status: "rolled-back", restoredAt: new Date().toISOString() });
|
|
110
|
+
console.log("Execution configuration restored; run doctor and continue/re-pin active tasks explicitly.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const to = value("--to");
|
|
114
|
+
if (to !== "standalone") throw new Error("Use --to standalone, or --rollback <migration-id>");
|
|
115
|
+
const previous = resolveExecution(target);
|
|
116
|
+
const selected = value("--model-profile");
|
|
117
|
+
if (selected) profilePolicy(target, selected);
|
|
118
|
+
const mode = value("--external-review") ?? (previous.name === "standalone" ? previous.externalReview.mode : "off");
|
|
119
|
+
const next = executionPolicy({ schemaVersion: 1, name: "standalone", externalReview: { mode },
|
|
120
|
+
acceptanceRequired: previous.name === "standalone" ? previous.acceptanceRequired : false,
|
|
121
|
+
requireObservedEffort: previous.name === "standalone" ? previous.requireObservedEffort : false,
|
|
122
|
+
maxParallelWorkers: previous.name === "standalone" ? previous.maxParallelWorkers : 2,
|
|
123
|
+
...(previous.designReviewRequired !== undefined ? { designReviewRequired: previous.designReviewRequired } : {}),
|
|
124
|
+
...(previous.reviewContractRequired !== undefined ? { reviewContractRequired: previous.reviewContractRequired } : {}) });
|
|
125
|
+
console.log(`Execution: ${previous.name} -> standalone; external review: ${previous.externalReview.mode} -> ${next.externalReview.mode}; model profile: ${previous.profileName} -> ${selected || previous.profileName}.`);
|
|
126
|
+
console.log("New tasks use Claude Code intake/plan, implementation or audit/plan mode, verification, independent review and report. Acceptance is required only by contract/policy. Existing tasks are not converted.");
|
|
127
|
+
reportTasks();
|
|
128
|
+
const id = crypto.randomUUID();
|
|
129
|
+
const directory = `.nccgs/backups/execution/${id}`;
|
|
130
|
+
const files = [...permitted].map(relative => {
|
|
131
|
+
const before = currentHash(relative);
|
|
132
|
+
at(`${directory}/files/${relative}`);
|
|
133
|
+
return { path: relative, before };
|
|
134
|
+
});
|
|
135
|
+
at(`${directory}/manifest.json`);
|
|
136
|
+
if (dryRun) { console.log("Dry run complete; no files changed."); return; }
|
|
137
|
+
const manifest = { schemaVersion: 1, kind: "execution-migration", id, status: "prepared", createdAt: new Date().toISOString(), from: previous,
|
|
138
|
+
to: { ...next, profileName: selected || previous.profileName }, activeTasks: tasks, files };
|
|
139
|
+
for (const item of files) if (item.before !== null) {
|
|
140
|
+
const backup = at(`${directory}/files/${item.path}`);
|
|
141
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
142
|
+
fs.copyFileSync(at(item.path), at(`${directory}/files/${item.path}`));
|
|
143
|
+
}
|
|
144
|
+
atomicJson(at(`${directory}/manifest.json`), manifest);
|
|
145
|
+
try {
|
|
146
|
+
atomicJson(at(".nccgs/execution.json"), { schemaVersion: 1, ...next });
|
|
147
|
+
if (selected) {
|
|
148
|
+
const result = spawnSync(process.execPath, [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), "--project", target, "--profile", selected], { cwd: target, encoding: "utf8" });
|
|
149
|
+
if (result.status !== 0) throw new Error(result.stderr || result.stdout || "Model reconfiguration failed");
|
|
150
|
+
process.stdout.write(result.stdout ?? "");
|
|
151
|
+
process.stderr.write(result.stderr ?? "");
|
|
152
|
+
}
|
|
153
|
+
for (const item of files) item.after = currentHash(item.path);
|
|
154
|
+
atomicJson(at(`${directory}/manifest.json`), { ...manifest, files, status: "applied", appliedAt: new Date().toISOString() });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
restore(manifest, directory);
|
|
157
|
+
atomicJson(at(`${directory}/manifest.json`), { ...manifest, status: "failed-restored", failure: error.message });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
console.log(`Execution migration ${id} applied. Rollback: nccgs migrate-execution --rollback ${id} --dry-run`);
|
|
161
|
+
}
|
|
42
162
|
|
|
43
163
|
function printHelp() {
|
|
44
164
|
console.log(`NCCGS
|
|
@@ -47,9 +167,14 @@ Usage:
|
|
|
47
167
|
nccgs install [options]
|
|
48
168
|
nccgs update [options]
|
|
49
169
|
nccgs doctor [options]
|
|
50
|
-
nccgs
|
|
170
|
+
nccgs run [--project DIR] [--claude EXECUTABLE] [--dry-run] -- [Claude arguments/prompt]
|
|
171
|
+
nccgs run --supervise --task ID --prompt-file FILE --session-budget-usd N [--max-resumes 2] [--timeout-ms 3600000] -- [provider options]
|
|
172
|
+
nccgs run --recover-run RUN_ID [--project DIR] [--claude EXECUTABLE]
|
|
173
|
+
nccgs task init|snapshot|ref|observe|verify|acceptance-status|record|check|close|bind|extend-review|refresh-inputs|migrate-evidence [options]
|
|
51
174
|
nccgs agents --enable <role> | --disable <role> [--project <directory>]
|
|
52
175
|
nccgs policy [--write] Check/regenerate package routing
|
|
176
|
+
nccgs migrate-execution --to standalone [--external-review off|advisory|required] [--model-profile <profile>] [--dry-run]
|
|
177
|
+
nccgs migrate-execution --rollback <migration-id> [--dry-run]
|
|
53
178
|
nccgs --version
|
|
54
179
|
|
|
55
180
|
Install target:
|
|
@@ -59,7 +184,9 @@ Install target:
|
|
|
59
184
|
Options:
|
|
60
185
|
--dry-run Preview without writing files
|
|
61
186
|
--force Back up and discard detected project overrides
|
|
62
|
-
--model-profile <profile>
|
|
187
|
+
--model-profile <profile> claude-standalone (new default), astra-claude, quality, balanced, inherit
|
|
188
|
+
--autonomy full|guarded Install/update: task authority for nccgs run (new install: full; update: preserve)
|
|
63
189
|
--project <directory> Optional explicit target directory
|
|
190
|
+
--risk <tier> task init: FAST, STANDARD (default), CONTROLLED
|
|
64
191
|
--allow-non-unity Allow a target without Unity markers`);
|
|
65
192
|
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -4,7 +4,10 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { matchesFingerprint, sameFingerprint } from "../.claude/nccgs/tools/integrity.mjs";
|
|
6
6
|
import { check as checkTask, snapshot, stages } from "../.claude/nccgs/tools/gates.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { hash, json, profilePolicy, resolveExecution, verifyArtifact } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
+
import { assertProjectRoot, assertRelativePath, containedPath } from "../.claude/nccgs/tools/paths.mjs";
|
|
9
|
+
import { inputPolicy } from "../.claude/nccgs/tools/inputs.mjs";
|
|
10
|
+
import { autonomyPolicy } from "../.claude/nccgs/tools/autonomy.mjs";
|
|
8
11
|
|
|
9
12
|
const args = process.argv.slice(2);
|
|
10
13
|
const projectIndex = args.indexOf("--project");
|
|
@@ -17,12 +20,15 @@ if (projectIndex >= 0 && !args[projectIndex + 1]) {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
|
|
23
|
+
try { assertProjectRoot(project); }
|
|
24
|
+
catch (error) { console.error(`ERROR: ${error.message}`); process.exit(1); }
|
|
20
25
|
for (const marker of ["Assets", "Packages", "ProjectSettings"]) {
|
|
21
|
-
|
|
26
|
+
const file = safePath(marker);
|
|
27
|
+
if (file && !fs.existsSync(file)) errors.push(`Missing Unity marker: ${marker}`);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
const statePath =
|
|
25
|
-
const overridePath =
|
|
30
|
+
const statePath = safePath(".claude/nccgs/install-state.json");
|
|
31
|
+
const overridePath = safePath(".nccgs/framework-overrides.json");
|
|
26
32
|
const state = readJson(statePath, "NCCGS install state", true);
|
|
27
33
|
const overrideManifest = readJson(overridePath, "NCCGS framework override manifest", false);
|
|
28
34
|
if (state && ![1, 2, 3].includes(state.schemaVersion)) errors.push(`Unsupported install-state schema: ${state.schemaVersion ?? "unknown"}.`);
|
|
@@ -46,7 +52,8 @@ if (state) {
|
|
|
46
52
|
errors.push(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
|
-
const file =
|
|
55
|
+
const file = safePath(item.relative);
|
|
56
|
+
if (!file) continue;
|
|
50
57
|
const override = overrides.get(item.relative);
|
|
51
58
|
if (override) {
|
|
52
59
|
const recordedUpstream = {
|
|
@@ -65,18 +72,19 @@ if (state) {
|
|
|
65
72
|
}
|
|
66
73
|
for (const item of overrides.values()) {
|
|
67
74
|
if ((state?.files ?? []).some((managed) => managed.relative === item.path)) continue;
|
|
68
|
-
|
|
75
|
+
const file = safePath(item.path);
|
|
76
|
+
if (file) validateOverride(item, file);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
const claudePath =
|
|
72
|
-
if (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md")) {
|
|
79
|
+
const claudePath = safePath("CLAUDE.md");
|
|
80
|
+
if (claudePath && (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md"))) {
|
|
73
81
|
errors.push("CLAUDE.md does not import the NCCGS constitution.");
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
const policyPath =
|
|
84
|
+
const policyPath = safePath(".nccgs/project.yaml");
|
|
77
85
|
let policySchema = null;
|
|
78
|
-
if (!fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
|
|
79
|
-
else {
|
|
86
|
+
if (policyPath && !fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
|
|
87
|
+
else if (policyPath) {
|
|
80
88
|
const policyText = fs.readFileSync(policyPath, "utf8");
|
|
81
89
|
const match = policyText.match(/^schema_version:\s*(\d+)\s*$/m);
|
|
82
90
|
policySchema = match ? Number(match[1]) : null;
|
|
@@ -84,24 +92,42 @@ else {
|
|
|
84
92
|
if (/^profile_contracts:/m.test(policyText)) migrationIssue("Legacy embedded profile_contracts require Astra migration to pipeline.json plus project gate mapping.");
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
validateSettings();
|
|
96
|
+
let execution = null;
|
|
97
|
+
try {
|
|
98
|
+
execution = resolveExecution(project);
|
|
99
|
+
if (!fs.existsSync(containedPath(project, ".nccgs/execution.json"))) warnings.push("No execution.json: compatibility obligations remain active. Standalone requires explicit migrate-execution; updating does not waive pending external review.");
|
|
100
|
+
if (!execution.requireObservedEffort) warnings.push("Execution permits unknown observed effort; configured effort is intent, not runtime proof. Report unavailable provider model/effort evidence as unknown.");
|
|
101
|
+
} catch (error) { errors.push(`Invalid execution policy: ${error.message}`); }
|
|
102
|
+
try { inputPolicy(project); }
|
|
103
|
+
catch (error) { errors.push(`Invalid canonical input/output policy: ${error.message}`); }
|
|
104
|
+
|
|
105
|
+
const activationPath = safePath(".nccgs/agent-activation.json");
|
|
106
|
+
if (activationPath && fs.existsSync(activationPath)) {
|
|
89
107
|
try {
|
|
90
108
|
const activation = json(activationPath);
|
|
91
109
|
for (const [relative, expected] of Object.entries(activation.generated)) {
|
|
92
|
-
const bytes = fs.readFileSync(
|
|
110
|
+
const bytes = fs.readFileSync(containedPath(project, relative));
|
|
93
111
|
if (hash(bytes) !== expected && hash(bytes.toString("utf8").replace(/\r\n/g, "\n")) !== activation.generatedCanonical?.[relative]) errors.push(`Activated agent modified: ${relative}`);
|
|
94
112
|
}
|
|
95
113
|
for (const warning of activation.warnings ?? []) migrationIssue(warning);
|
|
96
114
|
} catch (error) { errors.push(`Invalid agent activation state: ${error.message}`); }
|
|
97
115
|
}
|
|
98
|
-
const tasksDir =
|
|
99
|
-
if (fs.existsSync(tasksDir)) for (const file of fs.readdirSync(tasksDir).filter(f => f.endsWith(".json"))) {
|
|
116
|
+
const tasksDir = safePath(".nccgs/tasks");
|
|
117
|
+
if (tasksDir && fs.existsSync(tasksDir)) for (const file of fs.readdirSync(tasksDir).filter(f => f.endsWith(".json"))) {
|
|
100
118
|
try {
|
|
101
|
-
const task = json(
|
|
119
|
+
const task = json(containedPath(project, `.nccgs/tasks/${file}`));
|
|
120
|
+
if (task.schemaVersion !== 2 && execution?.name === "standalone") warnings.push(`Legacy task ${task.id} retains compatibility stages and budget; explicit continuation/re-pin is required to adopt standalone policy.`);
|
|
121
|
+
const executionCurrent = task.schemaVersion !== 2 || hash(JSON.stringify(execution)) === task.executionHash;
|
|
122
|
+
if (!executionCurrent) warnings.push(`Task ${task.id} is pinned to another execution policy; preserve its history and explicitly continue/re-pin before claiming completion.`);
|
|
123
|
+
if (!task.canonicalInputs) warnings.push(`Task ${task.id} predates canonical input pins; it is historical only until refresh-inputs and fresh evidence are recorded.`);
|
|
102
124
|
if (task.closed) {
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
const required = task.schemaVersion === 2 ? [task.mode === "implement" ? "implementation" : "analysis", "review", "report"] : stages;
|
|
126
|
+
const terminal = task.schemaVersion === 2 ? task.stages?.report : task.stages?.acceptance;
|
|
127
|
+
if (!required.every(s => task.stages?.[s]) || task.closed.snapshot !== terminal?.snapshot) throw new Error("Closure lacks its required stage records");
|
|
128
|
+
if (task.schemaVersion === 2 && (task.execution?.acceptanceRequired || task.contract?.acceptanceRequired) && !task.stages?.acceptance) throw new Error("Closure lacks required acceptance");
|
|
129
|
+
verifyStoredEvidence(task);
|
|
130
|
+
if (executionCurrent && task.canonicalInputs && task.closed.snapshot === snapshot(project, task).sha256) checkTask(project, task, "done");
|
|
105
131
|
else warnings.push(`Historical closure ${task.id} is for a different snapshot; it cannot approve current work.`);
|
|
106
132
|
}
|
|
107
133
|
} catch (error) { errors.push(`Invalid closed task ${file}: ${error.message}`); }
|
|
@@ -117,9 +143,12 @@ for (const warning of warnings) console.warn(`WARN: ${warning}`);
|
|
|
117
143
|
console.log("NCCGS doctor passed.");
|
|
118
144
|
console.log(`Framework version: ${state.frameworkVersion}`);
|
|
119
145
|
console.log(`Model profile: ${state.modelProfile}`);
|
|
146
|
+
console.log(`Execution policy: ${execution?.name}; external review: ${execution?.externalReview.mode}; maximum parallel workers: ${execution?.maxParallelWorkers}`);
|
|
147
|
+
console.log(`Reviewed design gate: ${execution?.name === "standalone" && execution?.designReviewRequired === true ? "required for feature implementation" : "legacy/compatibility policy; not proof of document readiness"}`);
|
|
120
148
|
console.log(`Managed files verified: ${verifiedManaged}`);
|
|
121
149
|
console.log(`Framework overrides: ${overrides.size} (${[...overrides.values()].filter((item) => item.status !== "approved").length} pending review)`);
|
|
122
150
|
console.log(`Project policy schema: ${policySchema ?? "migration required"}`);
|
|
151
|
+
console.log("Settings scope: project settings and observable local overrides only; CLI, user/managed settings, provider routing and active-session model/effort still require runtime verification.");
|
|
123
152
|
|
|
124
153
|
function validateOverride(item, file) {
|
|
125
154
|
if (item.status !== "approved") migrationIssue(`Framework override awaits /migrate-project review: ${item.path}`);
|
|
@@ -137,6 +166,7 @@ function migrationIssue(message) {
|
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
function readJson(file, label, required) {
|
|
169
|
+
if (!file) return null;
|
|
140
170
|
if (!fs.existsSync(file)) {
|
|
141
171
|
if (required) errors.push(`${label} is missing.`);
|
|
142
172
|
return null;
|
|
@@ -149,7 +179,83 @@ function readJson(file, label, required) {
|
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
function isManagedRelative(value) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
try { return assertRelativePath(value).startsWith(".claude/"); }
|
|
183
|
+
catch { return false; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function safePath(relative) {
|
|
187
|
+
try { return containedPath(project, relative); }
|
|
188
|
+
catch (error) { errors.push(`Unsafe NCCGS path: ${error.message}`); return null; }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function verifyStoredEvidence(task) {
|
|
192
|
+
const verify = (ref, runtime = false) => {
|
|
193
|
+
const file = verifyArtifact(project, ref);
|
|
194
|
+
if (!ref.path.endsWith(".json")) return;
|
|
195
|
+
const record = json(file);
|
|
196
|
+
if (runtime) {
|
|
197
|
+
verifyArtifact(project, record.observationSource);
|
|
198
|
+
if (record.evidenceMigration?.archivedObservation) verifyArtifact(project, record.evidenceMigration.archivedObservation);
|
|
199
|
+
} else if (record.origin === "verification-command") verifyArtifact(project, record.log);
|
|
200
|
+
};
|
|
201
|
+
for (const stage of Object.values(task.stages ?? {})) {
|
|
202
|
+
if (stage.runtime) verify(stage.runtime, true);
|
|
203
|
+
if (stage.evidence) verify(stage.evidence);
|
|
204
|
+
for (const check of stage.checks ?? []) verify(check.evidence);
|
|
205
|
+
for (const artifact of stage.artifacts ?? []) verify(artifact);
|
|
206
|
+
}
|
|
207
|
+
for (const receipt of task.externalReviews ?? []) verify(receipt.evidence);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateSettings() {
|
|
211
|
+
const settings = readJson(safePath(".claude/settings.json"), "Claude project settings", true);
|
|
212
|
+
const fragment = readJson(safePath(".claude/nccgs/settings.fragment.json"), "NCCGS settings fragment", true);
|
|
213
|
+
const local = readJson(safePath(".claude/settings.local.json"), "Claude local settings", false);
|
|
214
|
+
const object = value => value && typeof value === "object" && !Array.isArray(value);
|
|
215
|
+
if (!object(settings) || !object(fragment)) {
|
|
216
|
+
errors.push("Claude project settings and NCCGS settings fragment must be JSON objects.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (local !== null && !object(local)) errors.push("Claude local settings must be a JSON object.");
|
|
220
|
+
for (const [label, value] of [["project", settings], ["local", local]]) {
|
|
221
|
+
if (value?.disableAllHooks === true) errors.push(`Claude ${label} settings disable all hooks.`);
|
|
222
|
+
}
|
|
223
|
+
for (const [event, groups] of Object.entries(fragment.hooks ?? {})) {
|
|
224
|
+
for (const expected of groups) for (const hook of expected.hooks ?? []) {
|
|
225
|
+
const actualGroups = Array.isArray(settings.hooks?.[event]) ? settings.hooks[event] : [];
|
|
226
|
+
const present = actualGroups.some(group => group && (group.matcher ?? "") === (expected.matcher ?? "")
|
|
227
|
+
&& Array.isArray(group.hooks) && group.hooks.some(actual => actual && actual.type === hook.type && actual.command === hook.command
|
|
228
|
+
&& actual.async !== true && (actual.timeout === undefined || actual.timeout >= hook.timeout)));
|
|
229
|
+
if (!present) errors.push(`Required NCCGS hook missing or altered: ${event} [${expected.matcher ?? ""}] ${hook.command}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
let autonomy = "guarded";
|
|
233
|
+
try { autonomy = autonomyPolicy(project).mode; }
|
|
234
|
+
catch (error) { errors.push(error.message); }
|
|
235
|
+
for (const [key, required] of Object.entries(fragment.permissions ?? {})) {
|
|
236
|
+
for (const rule of required) if (!Array.isArray(settings.permissions?.[key]) || !settings.permissions[key].includes(rule)) {
|
|
237
|
+
errors.push(`Required NCCGS permission ${key} rule missing: ${rule}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (autonomy === "full") {
|
|
241
|
+
console.log("Task autonomy: full (native permission mode requires nccgs run; configuration is not runtime proof).");
|
|
242
|
+
console.log("NCCGS integrity/destructive-action guards, explicit deny/ask rules and managed/OS restrictions remain effective.");
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
if (!safePath(".claude/nccgs/pipeline.json")) return;
|
|
246
|
+
const { name, profile } = profilePolicy(project, state?.modelProfile);
|
|
247
|
+
const declared = policyPath && fs.existsSync(policyPath)
|
|
248
|
+
? fs.readFileSync(policyPath, "utf8").match(/^models:[ \t]*\r?\n((?:[ \t]+[^\n]*\n?|\s*\n)*)/m)?.[1]?.match(/^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "") : null;
|
|
249
|
+
if (declared && declared !== name) errors.push(`Project model profile ${declared} differs from installed profile ${name}; reconfigure models.`);
|
|
250
|
+
if (profile.session) {
|
|
251
|
+
const route = profile.models[profile.session.class];
|
|
252
|
+
if (settings.model !== route.model) errors.push(`Project session model must be ${route.model}; found ${settings.model ?? "missing"}.`);
|
|
253
|
+
if (settings.effortLevel !== route.effort) errors.push(`Project session effort must be ${route.effort}; found ${settings.effortLevel ?? "missing"}.`);
|
|
254
|
+
if (settings.modelSettings?.[route.model]?.effortLevel !== route.effort) errors.push(`Project per-model effort for ${route.model} must be ${route.effort}.`);
|
|
255
|
+
if (local?.model !== undefined && local.model !== route.model) errors.push(`Local settings override the required session model ${route.model}.`);
|
|
256
|
+
if (local?.effortLevel !== undefined && local.effortLevel !== route.effort) errors.push(`Local settings override the required session effort ${route.effort}.`);
|
|
257
|
+
if (local?.modelSettings?.[route.model]?.effortLevel !== undefined && local.modelSettings[route.model].effortLevel !== route.effort) errors.push(`Local settings override the required per-model effort for ${route.model}.`);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) { errors.push(`Cannot verify session model policy: ${error.message}`); }
|
|
260
|
+
if (local) warnings.push("Local Claude settings are present; project requirements were checked without treating local hook additions as replacements. Runtime overrides are not fully observable by doctor.");
|
|
155
261
|
}
|