@yunhozz/arc 0.1.9 → 0.1.10
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/MANIFEST.sha256 +85 -60
- package/README.md +1 -1
- package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
- package/claude/agents/arc-planner.md +4 -4
- package/claude/agents/arc-qa.md +4 -4
- package/claude/agents/arc-reviewer.md +5 -5
- package/claude/agents/arc-scouter.md +3 -3
- package/claude/skill/SKILL.md +7 -7
- package/claude/skill/models.toml +3 -3
- package/claude/skill/references/commit.md +22 -7
- package/claude/skill/references/coordination.md +25 -18
- package/claude/skill/references/git-recovery.md +1 -1
- package/claude/skill/references/handoff-prep.md +25 -2
- package/claude/skill/references/handoff.md +106 -24
- package/claude/skill/references/planning.md +73 -22
- package/claude/skill/references/verification.md +155 -17
- package/claude/skill/runtime/acceptance.mjs +68 -0
- package/claude/skill/runtime/assignment.mjs +42 -0
- package/claude/skill/runtime/bootstrap.mjs +35 -0
- package/claude/skill/runtime/cooperative-schema.mjs +55 -0
- package/claude/skill/runtime/cooperative.mjs +136 -0
- package/claude/skill/runtime/evaluate.mjs +40 -4
- package/claude/skill/runtime/external.mjs +51 -0
- package/claude/skill/runtime/gate.mjs +7 -7
- package/claude/skill/runtime/git-evidence.mjs +46 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +64 -19
- package/claude/skill/runtime/schema.mjs +1 -0
- package/claude/skill/runtime/scope.mjs +36 -0
- package/claude/skill/runtime/store.mjs +25 -4
- package/claude/skill/runtime/verification.mjs +123 -0
- package/claude/skill/scripts/model_config.py +2 -2
- package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
- package/codex/agents/arc-planner.toml +6 -6
- package/codex/agents/arc-qa.toml +6 -6
- package/codex/agents/arc-reviewer.toml +6 -6
- package/codex/agents/arc-scouter.toml +5 -5
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +3 -3
- package/codex/skill/references/commit.md +23 -8
- package/codex/skill/references/coordination.md +25 -18
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +25 -2
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +116 -34
- package/codex/skill/references/planning.md +76 -23
- package/codex/skill/references/verification.md +160 -22
- package/codex/skill/runtime/acceptance.mjs +68 -0
- package/codex/skill/runtime/assignment.mjs +42 -0
- package/codex/skill/runtime/bootstrap.mjs +35 -0
- package/codex/skill/runtime/cooperative-schema.mjs +55 -0
- package/codex/skill/runtime/cooperative.mjs +136 -0
- package/codex/skill/runtime/evaluate.mjs +40 -4
- package/codex/skill/runtime/external.mjs +51 -0
- package/codex/skill/runtime/gate.mjs +7 -7
- package/codex/skill/runtime/git-evidence.mjs +46 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +64 -19
- package/codex/skill/runtime/schema.mjs +1 -0
- package/codex/skill/runtime/scope.mjs +36 -0
- package/codex/skill/runtime/store.mjs +25 -4
- package/codex/skill/runtime/verification.mjs +123 -0
- package/codex/skill/scripts/model_config.py +3 -3
- package/lib/cli/root.mjs +1 -0
- package/lib/cli/workflow.mjs +21 -0
- package/lib/installer/manifest.mjs +1 -1
- package/lib/installer/plan.mjs +27 -13
- package/lib/installer/targets.mjs +1 -1
- package/lib/models/config.mjs +1 -1
- package/lib/models/profiles.mjs +1 -1
- package/lib/policy/placement.mjs +6 -6
- package/lib/policy/render.mjs +3 -3
- package/lib/policy/role-policy.json +64 -19
- package/lib/runtime/acceptance.mjs +68 -0
- package/lib/runtime/assignment.mjs +42 -0
- package/lib/runtime/bootstrap.mjs +35 -0
- package/lib/runtime/cooperative-schema.mjs +55 -0
- package/lib/runtime/cooperative.mjs +136 -0
- package/lib/runtime/evaluate.mjs +40 -4
- package/lib/runtime/external.mjs +51 -0
- package/lib/runtime/gate.mjs +7 -7
- package/lib/runtime/git-evidence.mjs +46 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/schema.mjs +1 -0
- package/lib/runtime/scope.mjs +36 -0
- package/lib/runtime/store.mjs +25 -4
- package/lib/runtime/verification.mjs +123 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"description": "Return design, spec and plan drafts to Orchestrator."
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"id": "
|
|
17
|
+
"id": "implementer",
|
|
18
18
|
"description": "Implement assigned approved changes and focused checks."
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"orchestrator",
|
|
34
34
|
"scouter",
|
|
35
35
|
"planner",
|
|
36
|
-
"
|
|
36
|
+
"implementer",
|
|
37
37
|
"qa",
|
|
38
38
|
"reviewer"
|
|
39
39
|
],
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"roles": [
|
|
59
59
|
"scouter",
|
|
60
60
|
"planner",
|
|
61
|
-
"
|
|
61
|
+
"implementer",
|
|
62
62
|
"qa",
|
|
63
63
|
"reviewer"
|
|
64
64
|
],
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"orchestrator",
|
|
115
115
|
"scouter",
|
|
116
116
|
"planner",
|
|
117
|
-
"
|
|
117
|
+
"implementer",
|
|
118
118
|
"qa",
|
|
119
119
|
"reviewer"
|
|
120
120
|
],
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"role-ownership"
|
|
151
151
|
],
|
|
152
152
|
"authority": "orchestrator",
|
|
153
|
-
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to
|
|
153
|
+
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before reusing Implementer, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
|
|
154
154
|
"controls": {
|
|
155
155
|
"codex": "prompt",
|
|
156
156
|
"claude": "prompt"
|
|
@@ -197,9 +197,9 @@
|
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
|
-
"id": "role.
|
|
200
|
+
"id": "role.implementer-implementation",
|
|
201
201
|
"roles": [
|
|
202
|
-
"
|
|
202
|
+
"implementer"
|
|
203
203
|
],
|
|
204
204
|
"effect": "allow",
|
|
205
205
|
"operations": [
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
"criterion-evidence"
|
|
234
234
|
],
|
|
235
235
|
"authority": "orchestrator",
|
|
236
|
-
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for
|
|
236
|
+
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck.",
|
|
237
237
|
"controls": {
|
|
238
238
|
"codex": "prompt",
|
|
239
239
|
"claude": "prompt"
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"orchestrator",
|
|
266
266
|
"scouter",
|
|
267
267
|
"planner",
|
|
268
|
-
"
|
|
268
|
+
"implementer",
|
|
269
269
|
"qa",
|
|
270
270
|
"reviewer"
|
|
271
271
|
],
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"qa-rechecks"
|
|
279
279
|
],
|
|
280
280
|
"authority": "orchestrator",
|
|
281
|
-
"text": "For significant/high-risk work without a plan:
|
|
281
|
+
"text": "For significant/high-risk work without a plan: Implementer \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
|
|
282
282
|
"controls": {
|
|
283
283
|
"codex": "prompt",
|
|
284
284
|
"claude": "prompt"
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"orchestrator",
|
|
291
291
|
"scouter",
|
|
292
292
|
"planner",
|
|
293
|
-
"
|
|
293
|
+
"implementer",
|
|
294
294
|
"qa",
|
|
295
295
|
"reviewer"
|
|
296
296
|
],
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
"required-qa"
|
|
304
304
|
],
|
|
305
305
|
"authority": "orchestrator",
|
|
306
|
-
"text": "For significant/high-risk plan work: each task uses
|
|
306
|
+
"text": "For significant/high-risk plan work: each task uses Implementer \u2192 fresh QA as required \u2192 Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
|
|
307
307
|
"controls": {
|
|
308
308
|
"codex": "prompt",
|
|
309
309
|
"claude": "prompt"
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"orchestrator",
|
|
316
316
|
"scouter",
|
|
317
317
|
"planner",
|
|
318
|
-
"
|
|
318
|
+
"implementer",
|
|
319
319
|
"qa",
|
|
320
320
|
"reviewer"
|
|
321
321
|
],
|
|
@@ -327,7 +327,52 @@
|
|
|
327
327
|
"reviewer-findings"
|
|
328
328
|
],
|
|
329
329
|
"authority": "orchestrator",
|
|
330
|
-
"text": "For final overall Reviewer findings on plan work, including behavior findings, use
|
|
330
|
+
"text": "For final overall Reviewer findings on plan work, including behavior findings, use Implementer \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
|
|
331
|
+
"controls": {
|
|
332
|
+
"codex": "prompt",
|
|
333
|
+
"claude": "prompt"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "planning.plan-amend",
|
|
338
|
+
"roles": [
|
|
339
|
+
"orchestrator"
|
|
340
|
+
],
|
|
341
|
+
"effect": "require",
|
|
342
|
+
"operations": [
|
|
343
|
+
"plan-update",
|
|
344
|
+
"plan-commit-amend"
|
|
345
|
+
],
|
|
346
|
+
"evidence": [
|
|
347
|
+
"change-classification",
|
|
348
|
+
"actual-approval-chronology",
|
|
349
|
+
"rewrite-safety",
|
|
350
|
+
"sha-mapping"
|
|
351
|
+
],
|
|
352
|
+
"authority": "orchestrator",
|
|
353
|
+
"text": "During approved task execution, mechanical plan progress records need no new user approval; changed goals, scope, criteria, method, risk, dependencies or required verification require revised-version approval and preparation before affected work. In both cases Orchestrator amends the original planning-document commit at a safe boundary, without a separate document or fixup commit. Preserve actual approval chronology, recoverable old/new contents, hashes, versions and SHA mappings; never retroactively authorize earlier work. Rewrite only unpublished, unshared local linear history with clear ownership and no merges; stop on unknown publication, ownership, unexpected state or replay conflicts, without force push or a fallback document commit. Preserve implementation task boundaries and revalidate affected evidence. See planning, handoff-prep, commit and verification for the full contract.",
|
|
354
|
+
"controls": {
|
|
355
|
+
"codex": "prompt",
|
|
356
|
+
"claude": "prompt"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"id": "planning.planner-brief",
|
|
361
|
+
"roles": [
|
|
362
|
+
"orchestrator"
|
|
363
|
+
],
|
|
364
|
+
"effect": "require",
|
|
365
|
+
"operations": [
|
|
366
|
+
"planner-brief",
|
|
367
|
+
"planner-feedback"
|
|
368
|
+
],
|
|
369
|
+
"evidence": [
|
|
370
|
+
"complete-stage-brief",
|
|
371
|
+
"prepared-target",
|
|
372
|
+
"preceding-approval"
|
|
373
|
+
],
|
|
374
|
+
"authority": "orchestrator",
|
|
375
|
+
"text": "Before formal drafting, Orchestrator supplies one complete stage-specific brief with original requirements and criteria IDs, approved/open decisions, verified target and ownership, document paths and preceding approved versions, current behavior and connected source/generated/runtime/installer/documentation surfaces, policy conflicts, depth, dependencies, checks, holds and exact deliverable. Preserve shared brief field order. Review the complete draft once and consolidate substantive feedback as changed fields with other fields marked unchanged. Preserve sequential design, spec and plan approvals; no fixed call cap or incomplete drafts, and no calls for already resolved questions.",
|
|
331
376
|
"controls": {
|
|
332
377
|
"codex": "prompt",
|
|
333
378
|
"claude": "prompt"
|
|
@@ -337,7 +382,7 @@
|
|
|
337
382
|
"platforms": {
|
|
338
383
|
"codex": {
|
|
339
384
|
"level": "prompt",
|
|
340
|
-
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified;
|
|
385
|
+
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Implementer and QA need shell for approved checks."
|
|
341
386
|
},
|
|
342
387
|
"claude": {
|
|
343
388
|
"level": "capability",
|
|
@@ -346,13 +391,13 @@
|
|
|
346
391
|
},
|
|
347
392
|
"review": {
|
|
348
393
|
"noPlan": [
|
|
349
|
-
"
|
|
394
|
+
"implementer",
|
|
350
395
|
"qa",
|
|
351
396
|
"reviewer",
|
|
352
397
|
"orchestrator-accept"
|
|
353
398
|
],
|
|
354
399
|
"planTask": [
|
|
355
|
-
"
|
|
400
|
+
"implementer",
|
|
356
401
|
"fresh-qa",
|
|
357
402
|
"orchestrator-accept",
|
|
358
403
|
"orchestrator-commit"
|
|
@@ -362,7 +407,7 @@
|
|
|
362
407
|
"reviewer"
|
|
363
408
|
],
|
|
364
409
|
"planFinding": [
|
|
365
|
-
"
|
|
410
|
+
"implementer",
|
|
366
411
|
"reviewer"
|
|
367
412
|
]
|
|
368
413
|
},
|
|
@@ -580,7 +625,7 @@
|
|
|
580
625
|
"orchestrator",
|
|
581
626
|
"scouter",
|
|
582
627
|
"planner",
|
|
583
|
-
"
|
|
628
|
+
"implementer",
|
|
584
629
|
"qa",
|
|
585
630
|
"reviewer"
|
|
586
631
|
],
|
|
@@ -36,6 +36,7 @@ const unique = (items,key) => items.every(x=>object(x)&&text(x[key])) && new Set
|
|
|
36
36
|
function approval(a){return object(a)&&text(a.id)&&['user-message','human-confirmation'].includes(a.source)&&text(a.messageRef)&&hash(a.contentHash)&&object(a.scope)&&text(a.scope.taskId)&&absolute(a.scope.target)&&strings(a.scope.operations)&&a.scope.operations.every(op=>OPERATIONS.includes(op))&&strings(a.scope.paths)&&a.scope.paths.every(absolute);}
|
|
37
37
|
/** Validate stored workflow data. Observed provenance must still be checked by evaluate. */
|
|
38
38
|
export function validateWorkflowState(s){
|
|
39
|
+
if(object(s)&&s.workflowKind!==undefined)fail('provider state cannot use cooperative contract');
|
|
39
40
|
if(!object(s)||s.schemaVersion!==1||!text(s.workflowId)||!Number.isSafeInteger(s.revision)||s.revision<0)fail('version or identity');
|
|
40
41
|
if(!object(s.versions)||['policy','runtime','adapter','policyHash','runtimeHash','adapterHash'].some(k=>!text(s.versions[k])))fail('versions');
|
|
41
42
|
if(['policyHash','runtimeHash','adapterHash'].some(k=>!hash(s.versions[k])))fail('hash algorithm');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
export const absolute=value=>typeof value==='string'&&path.isAbsolute(value)&&path.normalize(value)===value;
|
|
5
|
+
export const within=(base,file)=>absolute(file)&&!path.relative(base,file).split(path.sep).includes('..')&&!path.isAbsolute(path.relative(base,file));
|
|
6
|
+
/** Inspect every component, including ancestors outside the ARC namespace. */
|
|
7
|
+
export async function inspectPath(filename,{missing=false,directory=false}={}){
|
|
8
|
+
if(!absolute(filename))throw Error('Unsafe absolute path');
|
|
9
|
+
let cursor=path.parse(filename).root;
|
|
10
|
+
const parts=filename.slice(cursor.length).split(path.sep).filter(Boolean);
|
|
11
|
+
for(let i=0;i<parts.length;i++){
|
|
12
|
+
cursor=path.join(cursor,parts[i]);let stat;
|
|
13
|
+
try{stat=await fs.lstat(cursor);}catch(error){if(missing&&error.code==='ENOENT')return;throw error;}
|
|
14
|
+
if(stat.isSymbolicLink())throw Error('Unsafe symlink path');
|
|
15
|
+
if(i<parts.length-1||directory){if(!stat.isDirectory())throw Error('Unsafe directory type');}
|
|
16
|
+
else if(!stat.isFile())throw Error('State must be a regular file');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function validateWorkflowId(id){if(typeof id!=='string'||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(id))throw Error('Invalid workflow ID');return id;}
|
|
20
|
+
/** Shell-free, read-only observation; inherited Git target/config overrides are removed. */
|
|
21
|
+
export async function observeCooperativeTarget(cwd){
|
|
22
|
+
if(!absolute(cwd))throw Error('Absolute workspace required');await inspectPath(cwd,{directory:true});
|
|
23
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
|
|
24
|
+
Object.assign(env,{GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
25
|
+
const query=(args,detached=false)=>{const r=spawnSync('git',['--no-pager','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});if(detached&&r.status===1&&!r.error)return null;if(r.status!==0||r.error||!r.stdout.endsWith('\n')||r.stdout.slice(0,-1).includes('\n'))throw Error('Git target observation failed');return r.stdout.slice(0,-1);};
|
|
26
|
+
const target={realpath:query(['rev-parse','--show-toplevel']),gitCommonDir:query(['rev-parse','--path-format=absolute','--git-common-dir']),gitDir:query(['rev-parse','--absolute-git-dir']),head:query(['rev-parse','HEAD']),branch:query(['symbolic-ref','--quiet','--short','HEAD'],true)};
|
|
27
|
+
await validateObservedTarget(cwd,target);return target;
|
|
28
|
+
}
|
|
29
|
+
export async function validateObservedTarget(cwd,target){
|
|
30
|
+
if(!absolute(cwd)||!target||target.realpath!==cwd||!absolute(target.gitCommonDir)||!absolute(target.gitDir)||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(target.head)||!(target.branch===null||typeof target.branch==='string'&&target.branch))throw Error('Invalid observed target');
|
|
31
|
+
await inspectPath(cwd,{directory:true});await inspectPath(target.gitCommonDir,{directory:true});
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
export async function workflowStatePath(target,workflowId){
|
|
35
|
+
validateWorkflowId(workflowId);const statePath=path.join(target.gitCommonDir,'arc','workflows',workflowId,'state.json');await inspectPath(statePath,{missing:true});return statePath;
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {verifiedCooperativeReconciliation} from './acceptance.mjs';
|
|
1
2
|
import {verifiedReconciliation} from './gate.mjs';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -51,13 +52,33 @@ export async function transactWorkflow({statePath,expectedRevision,requestId,req
|
|
|
51
52
|
next.revision=state.revision+1;next.requests=prior?state.requests.map(r=>r.requestId===requestId?{requestId,requestFingerprint,decision}:r):[...state.requests,{requestId,requestFingerprint,decision}];
|
|
52
53
|
next.audit=[...state.audit,{requestId,revision:next.revision,decision:decision.decision,ruleIds:decision.ruleIds??[],result:'pending'}];
|
|
53
54
|
validateWorkflowState(next);
|
|
54
|
-
|
|
55
|
-
const handle=await fs.open(temporary,'wx',0o600);try{await handle.writeFile(JSON.stringify(next,null,2)+'\n');await handle.sync();}finally{await handle.close();}
|
|
56
|
-
await fs.rename(temporary,statePath);temporary=undefined;
|
|
57
|
-
const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
|
|
55
|
+
await saveDurableState({statePath,state:next});
|
|
58
56
|
return {state:next,decision,replayed:false};
|
|
59
57
|
}finally{
|
|
60
58
|
if(temporary)await fs.unlink(temporary).catch(()=>{});
|
|
61
59
|
if(lock){await lock.close();await fs.unlink(lockPath);}
|
|
62
60
|
}
|
|
63
61
|
}
|
|
62
|
+
|
|
63
|
+
/** Shared durability primitive. A post-rename sync error is reported as failure;
|
|
64
|
+
* the caller must re-read rather than assume rollback or repeat an effect. */
|
|
65
|
+
export async function saveDurableState({statePath,state,create=false,storage={}}){
|
|
66
|
+
const parent=path.dirname(statePath),temporary=path.join(parent,`.${path.basename(statePath)}.${crypto.randomUUID()}.tmp`);
|
|
67
|
+
let exists=false;
|
|
68
|
+
try{
|
|
69
|
+
const handle=await fs.open(temporary,'wx',0o600);exists=true;
|
|
70
|
+
try{await handle.writeFile(JSON.stringify(state,null,2)+'\n');await handle.sync();}finally{await handle.close();}
|
|
71
|
+
if(create){await (storage.link??fs.link)(temporary,statePath);await fs.unlink(temporary);}else await (storage.rename??fs.rename)(temporary,statePath);
|
|
72
|
+
exists=false;
|
|
73
|
+
const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
|
|
74
|
+
}finally{if(exists)await fs.unlink(temporary).catch(()=>{});}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Only a locally verified cooperative commit may advance the stored target. */
|
|
78
|
+
export function applyCooperativeCommitTransition({state,decision}){
|
|
79
|
+
if(decision?.decision!=='allow'||!verifiedCooperativeReconciliation(decision,state)||!decision.proposedTransition?.commit)throw Error('Verified cooperative reconciliation required');
|
|
80
|
+
const next=structuredClone(state),transition=decision.proposedTransition;
|
|
81
|
+
next.targetHistory.push({target:structuredClone(state.target),revision:state.revision+1,source:'local-observation',kind:'verified-commit-parent'});
|
|
82
|
+
next.target=structuredClone(transition.target);next.commits.push(structuredClone(transition.commit));next.tasks.find(t=>t.id===transition.commit.taskId).status='committed';
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {spawnSync} from 'node:child_process';
|
|
5
|
+
import {fingerprintRequest} from './store.mjs';
|
|
6
|
+
import {absolute,within,inspectPath} from './scope.mjs';
|
|
7
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
8
|
+
const text=v=>typeof v==='string'&&v.length>0;
|
|
9
|
+
const hash=v=>typeof v==='string'&&/^sha256:[a-f0-9]{64}$/.test(v);
|
|
10
|
+
const list=v=>Array.isArray(v)&&v.every(text)&&new Set(v).size===v.length;
|
|
11
|
+
const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).every(k=>keys.includes(k));
|
|
12
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
13
|
+
const hold=(state,id,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.'+id],reason});
|
|
14
|
+
const allow=(state,transition)=>({...base(state),decision:'allow',ruleIds:['cooperative.verification'],proposedTransition:transition});
|
|
15
|
+
const current=(state,scope,taskId,changes)=>scope==='final'?changes?.final:changes?.tasks?.[taskId];
|
|
16
|
+
const approval=(state,taskId)=>state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(taskId))).at(-1);
|
|
17
|
+
function authorized(state,task){
|
|
18
|
+
const a=approval(state,task?.id);
|
|
19
|
+
return task&&!task.invalidated&&a&&a.source==='operator-attested'&&a.targetVersion===state.targetVersion&&task.scopeVersion===a.scopeVersion&&task.targetVersion===state.targetVersion&&task.approvalRef===a.messageRef&&task.approvalVersion===a.data.approvalVersion&&task.documentVersion===a.data.documentVersion;
|
|
20
|
+
}
|
|
21
|
+
function descriptorValid(state,d,operation){
|
|
22
|
+
if(operation==='review')return fields(d,['kind','paths'])&&d.kind==='source-review'&&list(d.paths)&&d.paths.length>0&&d.paths.every(p=>absolute(p)&&within(state.target.realpath,p));
|
|
23
|
+
return fields(d,['executable','args','cwd','env','outputs'])&&absolute(d.executable)&&Array.isArray(d.args)&&d.args.every(v=>typeof v==='string')&&absolute(d.cwd)&&within(state.target.realpath,d.cwd)&&fields(d.env,Object.keys(d.env??{}))&&Object.values(d.env).every(v=>typeof v==='string')&&Array.isArray(d.outputs)&&d.outputs.every(p=>absolute(p)&&within(state.target.realpath,p));
|
|
24
|
+
}
|
|
25
|
+
const assignedImplementer=(state,id,taskId)=>state.assignments.some(a=>a.agentId===id&&a.taskId===taskId&&a.status==='active');
|
|
26
|
+
function independent(state,actor,taskId){
|
|
27
|
+
if(!fields(actor,['id','role'])||!text(actor.id))return false;
|
|
28
|
+
if(state.assignments.some(a=>a.agentId===actor.id))return false;
|
|
29
|
+
return !(state.intents??[]).some(i=>i.verificationActor?.id===actor.id&&(i.verificationActor.role!==actor.role||actor.role==='qa'&&i.taskId!==taskId))&&!(state.checks??[]).some(c=>c.verificationActor?.id===actor.id&&(c.verificationActor.role!==actor.role||actor.role==='qa'&&c.taskId!==taskId));
|
|
30
|
+
}
|
|
31
|
+
/** Invalidate only the affected dependency closure; preserve all historical evidence. */
|
|
32
|
+
export function invalidateAffectedEvidence({state,changedVersions={},affectedTaskIds}={}){
|
|
33
|
+
const next=structuredClone(state),ids=new Set(affectedTaskIds??next.tasks.map(t=>t.id));
|
|
34
|
+
let changed=true;while(changed){changed=false;for(const t of next.tasks)if(!ids.has(t.id)&&t.dependencies?.some(id=>ids.has(id))){ids.add(t.id);changed=true;}}
|
|
35
|
+
for(const t of next.tasks)if(ids.has(t.id)){t.invalidated=true;t.status='held';t.invalidatedBy=structuredClone(changedVersions);t.invalidatedAt=state.revision+1;}
|
|
36
|
+
for(const key of ['checks','acceptances'])for(const entry of next[key]??[])if(entry.scope==='final'||ids.has(entry.taskId)){entry.stale=true;entry.invalidatedBy=structuredClone(changedVersions);}
|
|
37
|
+
for(const intent of next.intents??[])if(['check','review'].includes(intent.operation)&&(intent.scope==='final'||ids.has(intent.taskId)))intent.stale=true;
|
|
38
|
+
// Old approval statements remain immutable; no change creates renewed authority.
|
|
39
|
+
if(Number.isSafeInteger(changedVersions.scopeVersion))next.scopeVersion=changedVersions.scopeVersion;
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
function record({state,request,evidence}){
|
|
43
|
+
const entry=request.evidence,d=entry?.data;
|
|
44
|
+
if(!entry||!text(entry.messageRef)||!d)return hold(state,'verification-record','Structured evidence required');
|
|
45
|
+
if(entry.kind==='acceptance-evidence'&&d.kind==='task'){
|
|
46
|
+
const t=d.task,previous=state.tasks.find(task=>task.id===t?.id),a=approval(state,t?.id);
|
|
47
|
+
if(previous?.invalidated&&(!a||!Number.isSafeInteger(a.revision)||a.revision<previous.invalidatedAt||a.scopeVersion!==state.scopeVersion))return hold(state,'task-approval-stale','Invalidated task requires a renewed approval statement');
|
|
48
|
+
if(!fields(t,['id','paths','criteria','requiredChecks','dependencies','approvalRef','approvalVersion','documentVersion','scopeVersion','targetVersion','status'])||!text(t.id)||!list(t.paths)||!t.paths.length||!t.paths.every(p=>absolute(p)&&within(state.target.realpath,p))||!list(t.criteria)||!t.criteria.length||!list(t.dependencies)||t.dependencies.includes(t.id)||t.dependencies.some(id=>!state.tasks.some(task=>task.id===id))||!Array.isArray(t.requiredChecks)||!t.requiredChecks.length||!t.requiredChecks.every(c=>descriptorValid(state,c,'check'))||!['pending',undefined].includes(t.status)||state.tasks.some(task=>task.id===t.id&&['accepted','committed'].includes(task.status))||!authorized(state,t))return hold(state,'task-contract','Current approval and explicit pending task contract required');
|
|
49
|
+
return allow(state,{task:{...structuredClone(t),status:'pending',source:'operator-attested',messageRef:entry.messageRef}});
|
|
50
|
+
}
|
|
51
|
+
if(entry.kind!=='finding')return hold(state,'unsupported','Check and review outcomes require durable intent/result');
|
|
52
|
+
if(d.action==='open'){
|
|
53
|
+
const check=state.checks.find(c=>c.id===d.checkId&&!c.stale);
|
|
54
|
+
if(!fields(d,['action','id','checkId','message'])||!text(d.id)||!text(d.message)||state.findings.some(f=>f.id===d.id)||!check)return hold(state,'finding','Finding must link an existing check or review');
|
|
55
|
+
return allow(state,{finding:{id:d.id,checkId:check.id,scope:check.scope,taskId:check.taskId,origin:check.scope==='final'&&check.operation==='review'?'final-review':'check',status:'open',message:d.message,messageRef:entry.messageRef,source:'operator-attested'}});
|
|
56
|
+
}
|
|
57
|
+
const finding=state.findings.find(f=>f.id===d.id);
|
|
58
|
+
const affected=finding?.scope==='final'?state.tasks.map(t=>t.id):[finding?.taskId];
|
|
59
|
+
if(!fields(d,['action','id','actor','changeFingerprint'])||d.action!=='resolve'||!finding||finding.status==='closed'||!fields(d.actor,['id','role'])||d.actor.role!=='implementer'||!affected.some(id=>assignedImplementer(state,d.actor.id,id))||!hash(d.changeFingerprint)||d.changeFingerprint!==current(state,finding.scope,finding.taskId,evidence?.changes))return hold(state,'finding-resolution','Assigned Implementer correction at the current fingerprint required');
|
|
60
|
+
return allow(state,{finding:{...finding,status:'resolved',resolutionRef:entry.messageRef,resolutionActor:structuredClone(d.actor),changeFingerprint:d.changeFingerprint}});
|
|
61
|
+
}
|
|
62
|
+
/** Pure cooperative gate: verifies recorded consistency, never identity or semantic sufficiency. */
|
|
63
|
+
export function evaluateCooperativeVerification({state,request,evidence}={}){
|
|
64
|
+
if(state?.mode!=='plan')return hold(state,'unsupported','Only the complete spec and plan path is supported');
|
|
65
|
+
if(!request||request.expectedRevision!==state.revision)return hold(state,'revision','Stale verification request');
|
|
66
|
+
if(request.actor?.role!=='orchestrator')return hold(state,'verification-actor','Orchestrator statement required');
|
|
67
|
+
if(request.operation==='verification-record')return record({state,request,evidence});
|
|
68
|
+
if(request.operation==='verification-result')return outcome({state,request,evidence});
|
|
69
|
+
if(!['check','review'].includes(request.operation))return hold(state,'unsupported','Unsupported verification operation');
|
|
70
|
+
const a=request.arguments,t=state.tasks.find(t=>t.id===request.taskId);
|
|
71
|
+
if(!fields(a,['scope','actor','descriptor','changeFingerprint','findingIds'])||!['task','final'].includes(a.scope)||!hash(a.changeFingerprint)||a.changeFingerprint!==current(state,a.scope,request.taskId,evidence?.changes)||!descriptorValid(state,a.descriptor,request.operation))return hold(state,'verification-fingerprint','Exact scope descriptor and current fingerprint required');
|
|
72
|
+
if(a.scope==='task'&&(!authorized(state,t)||t.status==='held'||request.operation==='review'||!t.requiredChecks.some(d=>same(d,a.descriptor))))return hold(state,'verification-task','Current task approval and exact assigned check required; Reviewer is final only');
|
|
73
|
+
if(a.scope==='final'&&(request.taskId!==undefined||request.operation!=='review'||!state.tasks.length||state.tasks.some(t=>!authorized(state,t)||!['accepted','committed'].includes(t.status))))return hold(state,'verification-final','Final review follows accepted tasks and required task checks');
|
|
74
|
+
const finalCorrection=a.scope==='final'&&state.findings.some(f=>f.origin==='final-review'&&f.status==='resolved'&&a.findingIds?.includes(f.id));
|
|
75
|
+
if(a.scope==='final'&&!finalCorrection&&state.tasks.some(task=>task.requiredChecks.some(descriptor=>!state.checks.some(c=>c.scope==='task'&&c.taskId===task.id&&c.operation==='check'&&!c.stale&&c.status==='passed'&&c.verdict==='proven'&&text(c.resultRef)&&c.verificationActor?.role==='qa'&&same(c.descriptor,descriptor)&&(c.changeFingerprint===evidence.changes?.tasks?.[task.id]||task.status==='committed'&&state.commits?.some(commit=>commit.taskId===task.id&&commit.observationSource==='local-git'&&commit.fingerprint===c.changeFingerprint&&commit.postCommitFingerprint===evidence.changes?.tasks?.[task.id]))))))return hold(state,'verification-final-checks','Initial final review requires current task QA evidence');
|
|
76
|
+
if(a.actor?.role!==(request.operation==='review'?'reviewer':'qa')||a.actor.id===request.actor.id||!independent(state,a.actor,request.taskId))return hold(state,'verification-independence','Independent actor statement and fresh task QA required');
|
|
77
|
+
const findings=state.findings.filter(f=>f.status!=='closed'&&(a.scope==='final'||f.scope==='task'&&f.taskId===request.taskId));
|
|
78
|
+
if(a.findingIds!==undefined&&(!list(a.findingIds)||a.findingIds.some(id=>!findings.some(f=>f.id===id))))return hold(state,'finding-recheck','Unknown finding recheck');
|
|
79
|
+
if(findings.some(f=>f.status!=='resolved'||!text(f.resolutionRef)||f.resolutionActor?.role!=='implementer'||f.changeFingerprint!==a.changeFingerprint||!a.findingIds?.includes(f.id)||f.origin==='final-review'&&request.operation!=='review'||f.origin!=='final-review'&&request.operation!=='check'))return hold(state,'finding-recheck','Implementer resolution and the required QA or final Reviewer recheck required');
|
|
80
|
+
if(state.intents.some(i=>['check','review'].includes(i.operation)&&['prepared','unknown','mismatch'].includes(i.status)&&i.scope===a.scope&&i.taskId===request.taskId&&same(i.descriptor,a.descriptor)&&!i.stale))return hold(state,'verification-active','Reconcile the existing verification intent');
|
|
81
|
+
const intent={requestId:request.requestId,operation:request.operation,status:'prepared',scope:a.scope,taskId:request.taskId,verificationActor:structuredClone(a.actor),descriptor:structuredClone(a.descriptor),changeFingerprint:a.changeFingerprint,findingIds:[...(a.findingIds??[])],scopeVersion:state.scopeVersion,targetVersion:state.targetVersion,versions:structuredClone(state.versions),target:structuredClone(state.target),approval:structuredClone(approval(state,request.taskId)),taskFingerprint:fingerprintRequest(a.scope==='final'?state.tasks:t),fingerprint:fingerprintRequest(request),source:'operator-attested'};
|
|
82
|
+
return allow(state,{intent});
|
|
83
|
+
}
|
|
84
|
+
function outcome({state,request,evidence}){
|
|
85
|
+
const intent=state.intents.find(i=>i.requestId===request.intentId),r=request.result,d=r?.data;
|
|
86
|
+
if(!intent||!['check','review'].includes(intent.operation)||!['prepared','unknown','mismatch'].includes(intent.status))return hold(state,'verification-intent','Matching unresolved verification intent required');
|
|
87
|
+
const valid=r&&['succeeded','failed','unknown','mismatch'].includes(r.status)&&text(r.messageRef)&&fields(d,['actor','descriptor','scope','taskId','changeFingerprint','verdict'])&&['proven','unmet','unverified'].includes(d.verdict)&&same(d.actor,intent.verificationActor)&&same(d.descriptor,intent.descriptor)&&d.scope===intent.scope&&d.taskId===intent.taskId&&d.changeFingerprint===intent.changeFingerprint;
|
|
88
|
+
const fresh=!intent.stale&&intent.changeFingerprint===current(state,intent.scope,intent.taskId,evidence?.changes)&&(intent.scope==='task'||intent.scopeVersion===state.scopeVersion)&&intent.targetVersion===state.targetVersion&&same(intent.target,state.target)&&same(intent.versions,state.versions)&&independent(state,intent.verificationActor,intent.taskId)&&same(intent.approval,approval(state,intent.taskId))&&intent.taskFingerprint===fingerprintRequest(intent.scope==='final'?state.tasks:state.tasks.find(t=>t.id===intent.taskId));
|
|
89
|
+
const status=!valid?'mismatch':r.status;
|
|
90
|
+
const check={id:intent.requestId,operation:intent.operation,scope:intent.scope,taskId:intent.taskId,descriptor:structuredClone(intent.descriptor),verificationActor:structuredClone(intent.verificationActor),changeFingerprint:intent.changeFingerprint,resultRef:r?.messageRef,verdict:valid?d.verdict:'unverified',status:fresh&&status==='succeeded'&&d.verdict==='proven'?'passed':status==='failed'||d?.verdict==='unmet'?'failed':'unverified',stale:!fresh,source:'operator-attested'};
|
|
91
|
+
const findings=[];
|
|
92
|
+
if(check.status==='failed')findings.push({id:intent.requestId+':failure',checkId:check.id,scope:intent.scope,taskId:intent.taskId,origin:intent.operation==='review'?'final-review':'check',status:'open',source:'operator-attested',messageRef:r.messageRef});
|
|
93
|
+
if(check.status==='passed')for(const id of intent.findingIds){const f=state.findings.find(f=>f.id===id);if(!f||f.status!=='resolved'||f.changeFingerprint!==intent.changeFingerprint)return hold(state,'finding-recheck','Finding changed after prepare');findings.push({...f,status:'closed',recheckRef:r.messageRef,recheckActor:structuredClone(d.actor)});}
|
|
94
|
+
const transition={result:{...structuredClone(r),status,source:'operator-attested'},check,findings};
|
|
95
|
+
return check.status==='passed'?allow(state,transition):{...hold(state,'verification-'+status,'Recorded outcome does not establish passing current evidence'),proposedTransition:transition};
|
|
96
|
+
}
|
|
97
|
+
function defaultRunner({cwd,args}){
|
|
98
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([k])=>!k.toUpperCase().startsWith('GIT_')));
|
|
99
|
+
Object.assign(env,{GIT_OPTIONAL_LOCKS:'0',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
100
|
+
return spawnSync('git',['--no-pager','--literal-pathspecs','-c','core.fsmonitor=false',...args],{cwd,env,encoding:'buffer',timeout:10000,maxBuffer:16*1024*1024});
|
|
101
|
+
}
|
|
102
|
+
/** Read tracked/untracked regular bytes, deletions and modes without staging or execution.
|
|
103
|
+
* The snapshot is cooperative evidence at observation time, not an atomic execution sandbox. */
|
|
104
|
+
export async function readCooperativeChanges({state,gitRunner=defaultRunner}){
|
|
105
|
+
const inventory=await gitRunner({cwd:state.target.realpath,args:['ls-files','--cached','--others','--exclude-standard','-z']});
|
|
106
|
+
if(inventory?.status!==0||inventory.error||!Buffer.isBuffer(inventory.stdout))throw Error('Verification inventory query failed');
|
|
107
|
+
const raw=new TextDecoder('utf-8',{fatal:true}).decode(inventory.stdout);
|
|
108
|
+
if(raw&&!raw.endsWith('\0'))throw Error('Incomplete verification inventory');
|
|
109
|
+
const index=await gitRunner({cwd:state.target.realpath,args:['ls-files','--stage','-z']});
|
|
110
|
+
if(index?.status!==0||index.error||!Buffer.isBuffer(index.stdout))throw Error('Verification index query failed');
|
|
111
|
+
const indexText=new TextDecoder('utf-8',{fatal:true}).decode(index.stdout),indexRows=[];
|
|
112
|
+
if(indexText&&!indexText.endsWith('\0'))throw Error('Incomplete verification index');
|
|
113
|
+
for(const entry of indexText?indexText.slice(0,-1).split('\0'):[]){const match=/^(100644|100755) ([a-f0-9]{40}|[a-f0-9]{64}) 0\t([\s\S]+)$/.exec(entry);if(!match)throw Error('Verification requires regular stage-0 index entries');indexRows.push({path:path.join(state.target.realpath,match[3]),entry});}
|
|
114
|
+
const names=[...new Set(raw?raw.slice(0,-1).split('\0'):[])].sort(),rows=[];
|
|
115
|
+
for(const name of names){
|
|
116
|
+
if(!name||name.includes('\\')||name.includes('\n')||name.includes('\r')||path.isAbsolute(name)||name.split('/').some(p=>!p||p==='.'||p==='..'))throw Error('Invalid verification path');
|
|
117
|
+
const file=path.join(state.target.realpath,...name.split('/'));
|
|
118
|
+
try{await inspectPath(file);const stat=await fs.lstat(file);if(!stat.isFile())throw Error('Verification requires regular files');rows.push({path:file,mode:stat.mode&0o777,hash:'sha256:'+createHash('sha256').update(await fs.readFile(file)).digest('hex')});}
|
|
119
|
+
catch(error){if(error.code!=='ENOENT')throw error;rows.push({path:file,deleted:true});}
|
|
120
|
+
}
|
|
121
|
+
const tasks={},taskSnapshots={};for(const task of state.tasks){if(!list(task.paths)||!task.paths.every(p=>absolute(p)&&within(state.target.realpath,p)))throw Error('Invalid task paths');taskSnapshots[task.id]={scope:'task',taskId:task.id,target:Object.fromEntries(Object.entries(state.target).filter(([key])=>key!=='head')),paths:task.paths,index:indexRows.filter(row=>task.paths.some(p=>within(p,row.path))),rows:rows.filter(row=>task.paths.some(p=>within(p,row.path)))};tasks[task.id]=fingerprintRequest(taskSnapshots[task.id]);}
|
|
122
|
+
return {tasks,taskSnapshots,final:fingerprintRequest({scope:'final',target:state.target,index:indexRows,rows})};
|
|
123
|
+
}
|
|
@@ -13,7 +13,7 @@ from typing import TypedDict
|
|
|
13
13
|
ROLE_FILES: dict[str, str] = {
|
|
14
14
|
"arc_scouter": "arc-scouter.toml",
|
|
15
15
|
"arc_planner": "arc-planner.toml",
|
|
16
|
-
"
|
|
16
|
+
"arc_implementer": "arc-implementer.toml",
|
|
17
17
|
"arc_qa": "arc-qa.toml",
|
|
18
18
|
"arc_reviewer": "arc-reviewer.toml",
|
|
19
19
|
}
|
|
@@ -95,13 +95,13 @@ def validate_config(config: dict, path: Path) -> dict:
|
|
|
95
95
|
def load_settings(config_path: Path) -> dict[str, RoleSettings]:
|
|
96
96
|
"""Validate every preset and return the active role settings."""
|
|
97
97
|
_, _, config = _parse_file(config_path, "settings")
|
|
98
|
-
validate_config(config, config_path)
|
|
98
|
+
config = validate_config(config, config_path)
|
|
99
99
|
return {role: dict(variants[config["preset"]]) for role, variants in config["roles"].items()}
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def serialize_settings(config: dict) -> bytes:
|
|
103
103
|
"""Serialize a complete unified configuration without losing inactive presets."""
|
|
104
|
-
validate_config(config, Path("models.toml"))
|
|
104
|
+
config = validate_config(config, Path("models.toml"))
|
|
105
105
|
lines = ["version = 1", f'preset = {json.dumps(config["preset"])}']
|
|
106
106
|
for role in ROLE_FILES:
|
|
107
107
|
for preset in PRESETS:
|
package/lib/cli/root.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export async function runRoot(args=process.argv.slice(2),io={}){
|
|
|
7
7
|
const [command,...rest]=args;
|
|
8
8
|
if(command==='--help'&&!rest.length){output.write(usage+'\n\n');return(await import('./index.mjs')).runCli(args,io);}
|
|
9
9
|
if(command==='--version'&&!rest.length){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
|
|
10
|
+
if(command==='workflow')return(await import('./workflow.mjs')).runCli(rest,io);
|
|
10
11
|
if(command==='preset')return(await import('./preset.mjs')).runCli(rest,io);
|
|
11
12
|
if(command==='upgrade')return(await import('./upgrade.mjs')).runCli(rest,io);
|
|
12
13
|
if(command==='install'){error.write('Error: The install subcommand was removed. Run npx --yes @yunhozz/arc [installation options].\n');return 2;}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {initializeCooperativeWorkflow,commandResult} from '../runtime/bootstrap.mjs';
|
|
4
|
+
import {runCooperativeCommand} from '../runtime/cooperative.mjs';
|
|
5
|
+
import {validateWorkflowId} from '../runtime/scope.mjs';
|
|
6
|
+
export async function runCli(args,io={}){
|
|
7
|
+
const output=io.output??process.stdout;let workflowId=null;
|
|
8
|
+
try{
|
|
9
|
+
const [command,...rest]=args,options={};
|
|
10
|
+
if(!['init','record','status','reconcile','prepare','result'].includes(command))throw Error('Expected workflow command');
|
|
11
|
+
for(let i=0;i<rest.length;i+=2){const key=rest[i];if(!['--cwd','--workflow','--input'].includes(key)||options[key]!==undefined||typeof rest[i+1]!=='string')throw Error('Invalid workflow option');options[key]=rest[i+1];}
|
|
12
|
+
const cwd=options['--cwd'];workflowId=options['--workflow'];validateWorkflowId(workflowId);
|
|
13
|
+
if(typeof cwd!=='string'||!path.isAbsolute(cwd)||path.normalize(cwd)!==cwd)throw Error('Absolute workspace required');
|
|
14
|
+
if(command==='status'?options['--input']!==undefined:options['--input']!=='-')throw Error('Mutations require --input -; status takes no input');
|
|
15
|
+
// Check/review, commit and external intents are recorded for the operator.
|
|
16
|
+
// Actual Git mutations remain Orchestrator actions; reconciliation observes their results.
|
|
17
|
+
const input=command==='status'?undefined:JSON.parse(io.input??fs.readFileSync(0,'utf8'));
|
|
18
|
+
const result=await(command==='init'?initializeCooperativeWorkflow({cwd,workflowId,input}):runCooperativeCommand({cwd,workflowId,command,input}));
|
|
19
|
+
output.write(JSON.stringify(result)+'\n');return result.decision==='deny'?2:result.decision==='hold'?3:0;
|
|
20
|
+
}catch(error){output.write(JSON.stringify(commandResult({workflowId,decision:'hold',ruleId:'cooperative.error',error:error.message}))+'\n');return 1;}
|
|
21
|
+
}
|
|
@@ -25,7 +25,7 @@ export function verifyPayload({packageRoot,safeFs}){
|
|
|
25
25
|
for(const name of listed.keys())if(!files.has(name))throw Error(`Manifest entry is missing: ${name}`);
|
|
26
26
|
const required=['claude/skill/hooks/role-git-guard.mjs','claude/skill/hooks/git-policy.json','lib/policy/claude-hook.mjs','lib/policy/role-policy.json','lib/policy/render.mjs','scripts/update-role-policy.mjs','lib/limits.mjs','lib/installer/limits.mjs','lib/installer/runtime-config.mjs','README.md','install.command','package.json','bin/arc.mjs','lib/cli/root.mjs','lib/cli/completion.mjs','lib/installer/shell-completion.mjs','lib/installer/manifest.mjs','lib/installer/targets.mjs','lib/installer/global-config.mjs','lib/installer/plan.mjs','lib/safe-fs/index.mjs','lib/safe-fs/native.mjs','lib/models/index.mjs','lib/models/config.mjs','lib/models/profiles.mjs','vendor/toml/LICENSE','vendor/toml/package.json','vendor/toml/provenance.json','vendor/toml/dist/index.js','prebuilds/index.json'];
|
|
27
27
|
for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
|
|
28
|
-
const modelInputs={};for(const platform of ['codex','claude']){const base=`${platform}/skill`;required.push(`${base}/SKILL.md`,`${base}/models.toml`,`${base}/references/limits.md`);const profiles=new Map();for(const role of ['scouter','planner','
|
|
28
|
+
const modelInputs={};for(const platform of ['codex','claude']){const base=`${platform}/skill`;required.push(`${base}/SKILL.md`,`${base}/models.toml`,`${base}/references/limits.md`);const profiles=new Map();for(const role of ['scouter','planner','implementer','qa','reviewer']){const name=`arc-${role}.${platform==='codex'?'toml':'md'}`,key=`${platform}/agents/${name}`;required.push(key);profiles.set(name,files.get(key));}modelInputs[platform]={models:files.get(`${base}/models.toml`),profiles};}
|
|
29
29
|
for(const name of required)if(!files.has(name))throw Error(`Required runtime asset is missing: ${name}`);
|
|
30
30
|
const policyInputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,files.get(name)]));
|
|
31
31
|
const policyOutputs=buildPolicyOutputs(policyInputs,JSON.parse(files.get('lib/policy/role-policy.json').toString('utf8')));
|
package/lib/installer/plan.mjs
CHANGED
|
@@ -17,23 +17,19 @@ export function previewLanguage({target,safeFs}){
|
|
|
17
17
|
let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
|
|
18
18
|
return{existing,snapshot};
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
validateLanguage(language);
|
|
22
|
-
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
23
|
-
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
24
|
-
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
25
|
-
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
26
|
-
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
20
|
+
function renderModelAssets({payload,target,preset,modelConfig,language}){
|
|
27
21
|
const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
|
|
28
22
|
const config=modelConfig===undefined?applyPreset(loadModelConfig(input.models,target.platform),preset):validateModelConfig(modelConfig);if(config.platform!==target.platform)fail('Model config platform does not match target');const profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
|
|
29
23
|
if(target.platform==='claude'){try{const script=payload.files.get('claude/skill/hooks/role-git-guard.mjs'),policy=payload.files.get('claude/skill/hooks/git-policy.json');if(!Buffer.isBuffer(script)||!script.length||!Buffer.isBuffer(policy))throw Error('Missing hook assets');validateGitPolicy(JSON.parse(policy.toString('utf8')));for(const[name,bytes]of profiles)profiles.set(name,installClaudeHook(bytes,path.join(target.home,'skills/arc')));}catch(cause){fail(`Invalid Claude Git guard: ${cause.message}`);}}
|
|
24
|
+
return{profiles,modelBytes};
|
|
25
|
+
}
|
|
26
|
+
function renderSkillPayload({payload,target,language,modelBytes,limits}){
|
|
30
27
|
const skill=new Map();for(const[name,bytes]of payload.files)if(name.startsWith(target.payloadPrefix+'/skill/'))skill.set(name.slice((target.payloadPrefix+'/skill/').length),Buffer.from(bytes));skill.set('SKILL.md',renderLanguagePolicy(skill.get('SKILL.md'),language));skill.set('settings.toml',serializeLanguage(language));skill.set('models.toml',modelBytes);skill.set('limits.toml',serializeLimits(limits));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const globalChanges=[];summary.push(`ARC limits: total ${limits.max_total_subagents}; concurrent ${limits.max_concurrent_subagents} (policy: ${arc}/limits.toml)`);
|
|
28
|
+
return skill;
|
|
29
|
+
}
|
|
30
|
+
function renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurrentExplicit,limits,summary,language}){
|
|
31
|
+
const home=target.home;
|
|
32
|
+
const globalChanges=[];
|
|
37
33
|
if(limits.max_concurrent_subagents>limits.max_total_subagents)summary.push(`Effective concurrency is at most ${limits.max_total_subagents} for a fresh ARC session; it may be lower after cumulative use or due to native capacity.`);
|
|
38
34
|
if(skipGlobal)summary.push('Native concurrency update skipped (--skip-global).');
|
|
39
35
|
else for(const filename of target.globalPaths){
|
|
@@ -50,6 +46,24 @@ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,
|
|
|
50
46
|
if(rendered.content!==null&&(original===null||!rendered.content.equals(original)))globalChanges.push({path:filename,original,content:rendered.content,action:rendered.action});
|
|
51
47
|
for(const note of rendered.notes??[])summary.push(`${filename}: ${note}`);
|
|
52
48
|
}
|
|
49
|
+
return globalChanges;
|
|
50
|
+
}
|
|
51
|
+
export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
|
|
52
|
+
validateLanguage(language);
|
|
53
|
+
if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
|
|
54
|
+
const languageState=languagePreview??previewLanguage({target,safeFs});
|
|
55
|
+
if(limitsPreview)assertPlanSnapshot({snapshot:limitsPreview.snapshot},{safeFs});
|
|
56
|
+
const preview=limitsPreview??previewLimits({target,safeFs});
|
|
57
|
+
limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
|
|
58
|
+
const {profiles,modelBytes}=renderModelAssets({payload,target,preset,modelConfig,language});
|
|
59
|
+
const skill=renderSkillPayload({payload,target,language,modelBytes,limits});
|
|
60
|
+
const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
|
|
61
|
+
for(const name of [home,skills,agents])inspectPath(safeFs,name,{kind:'directory',snapshot});
|
|
62
|
+
inspectPath(safeFs,arc,{kind:'directory',tree:true,allowLinks:true,snapshot});
|
|
63
|
+
const profilePaths=target.profileNames.map(name=>path.join(agents,name));for(const name of profilePaths)inspectPath(safeFs,name,{kind:'file',snapshot});
|
|
64
|
+
const externalPaths=[],summary=['Fresh superpowers references will be acquired inside ARC'];
|
|
65
|
+
summary.push(`ARC limits: total ${limits.max_total_subagents}; concurrent ${limits.max_concurrent_subagents} (policy: ${arc}/limits.toml)`);
|
|
66
|
+
const globalChanges=renderNativeChanges({target,safeFs,snapshot,skipGlobal,preview,concurrentExplicit,limits,summary,language});
|
|
53
67
|
const managedPaths=[arc,...profilePaths,...(skipGlobal?[]:target.globalPaths)];
|
|
54
68
|
for(const [filename,metadata]of preview.snapshot)snapshot.set(filename,metadata);
|
|
55
69
|
for(const [filename,metadata]of languageState.snapshot)snapshot.set(filename,metadata);
|
|
@@ -9,7 +9,7 @@ export function resolveTarget(platform,{userHome,env={},cwd}={}){
|
|
|
9
9
|
let home;if(platform==='codex'){const raw=env.CODEX_HOME;if(raw!==undefined&&(typeof raw!=='string'||!raw.trim()))fail('CODEX_HOME is empty or invalid');const expanded=expand(raw??path.join(userHome,'.codex'));if(path.isAbsolute(expanded))home=path.resolve(expanded);else{const base=cwd??process.cwd();if(typeof base!=='string'||!path.isAbsolute(base))fail('cwd must be absolute for relative CODEX_HOME');home=path.resolve(base,expanded);}}
|
|
10
10
|
else{home=path.join(userHome,'.claude');const raw=env.CLAUDE_CONFIG_DIR;if(raw!==undefined&&(typeof raw!=='string'||!raw.trim()||!path.isAbsolute(expand(raw))||path.resolve(expand(raw))!==home))fail('CLAUDE_CONFIG_DIR must select the default ~/.claude directory');}
|
|
11
11
|
if(home==='/'||home===path.resolve(userHome))fail('Target home must not be the filesystem or user-home root');
|
|
12
|
-
const profileNames=['scouter','planner','
|
|
12
|
+
const profileNames=['scouter','planner','implementer','qa','reviewer'].map(role=>`arc-${role}.${platform==='codex'?'toml':'md'}`);
|
|
13
13
|
return Object.freeze({platform,home,userHome:path.resolve(userHome),payloadPrefix:platform,profileNames:Object.freeze(profileNames),instructionPath:path.join(home,platform==='codex'?'AGENTS.md':'CLAUDE.md'),runtimeConfigPath:path.join(home,platform==='codex'?'config.toml':'settings.json'),globalPaths:Object.freeze(platform==='codex'?[path.join(home,'AGENTS.md'),path.join(home,'config.toml')]:[path.join(home,'CLAUDE.md'),path.join(home,'settings.json')])});
|
|
14
14
|
}
|
|
15
15
|
export function typeOf(metadata){return Number(metadata.mode&0o170000n);}
|
package/lib/models/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse, stringify } from '../../vendor/toml/dist/index.js';
|
|
2
|
-
export const ROLES = Object.freeze(['arc_scouter', 'arc_planner', '
|
|
2
|
+
export const ROLES = Object.freeze(['arc_scouter', 'arc_planner', 'arc_implementer', 'arc_qa', 'arc_reviewer']);
|
|
3
3
|
export class ModelValidationError extends Error {
|
|
4
4
|
constructor(message, options) { super(message, options); this.name = 'ModelValidationError'; this.code = 'ARC_MODEL_VALIDATION'; }
|
|
5
5
|
}
|
package/lib/models/profiles.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { parseToml, exactKeys, validateRoles, effortField, ROLES, fail } from '.
|
|
|
3
3
|
const tools = {
|
|
4
4
|
arc_scouter: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch'],
|
|
5
5
|
arc_planner: ['Read', 'Grep', 'Glob'],
|
|
6
|
-
|
|
6
|
+
arc_implementer: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit'],
|
|
7
7
|
arc_qa: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
8
8
|
arc_reviewer: ['Read', 'Grep', 'Glob'],
|
|
9
9
|
};
|