audit-tools 0.28.2 → 0.28.3
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/dist/audit/cli/auditStep.d.ts +12 -1
- package/dist/audit/cli/auditStep.d.ts.map +1 -1
- package/dist/audit/cli/auditStep.js +1 -0
- package/dist/audit/cli/auditStep.js.map +1 -1
- package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
- package/dist/audit/cli/confirmIntentStep.js +46 -35
- package/dist/audit/cli/confirmIntentStep.js.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +5 -1
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/steps.d.ts +2 -2
- package/dist/audit/contracts/workerSchemas.d.ts +14 -14
- package/dist/audit/contracts/wrapperResponse.d.ts +4 -4
- package/dist/audit/extractors/analyzers/types.d.ts +3 -1
- package/dist/audit/extractors/analyzers/types.d.ts.map +1 -1
- package/dist/audit/extractors/analyzers/types.js +10 -1
- package/dist/audit/extractors/analyzers/types.js.map +1 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts +8 -0
- package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +1 -1
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts +19 -8
- package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/intentCheckpointExecutor.js +64 -40
- package/dist/audit/orchestrator/intentCheckpointExecutor.js.map +1 -1
- package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
- package/dist/audit/reporting/synthesis.d.ts.map +1 -1
- package/dist/audit/reporting/synthesis.js +91 -26
- package/dist/audit/reporting/synthesis.js.map +1 -1
- package/dist/audit/types/analyzerCapability.d.ts +69 -12
- package/dist/audit/types/analyzerCapability.d.ts.map +1 -1
- package/dist/audit/types/analyzerCapability.js +25 -1
- package/dist/audit/types/analyzerCapability.js.map +1 -1
- package/dist/audit/types/auditScope.d.ts +51 -12
- package/dist/audit/types/auditScope.d.ts.map +1 -1
- package/dist/audit/types/auditScope.js +49 -1
- package/dist/audit/types/auditScope.js.map +1 -1
- package/dist/audit/types/auditState.d.ts +60 -11
- package/dist/audit/types/auditState.d.ts.map +1 -1
- package/dist/audit/types/auditState.js +24 -1
- package/dist/audit/types/auditState.js.map +1 -1
- package/dist/audit/types/flowCoverage.d.ts +65 -8
- package/dist/audit/types/flowCoverage.d.ts.map +1 -1
- package/dist/audit/types/flowCoverage.js +21 -0
- package/dist/audit/types/flowCoverage.js.map +1 -1
- package/dist/audit/types/reviewPlanning.d.ts +2 -2
- package/dist/audit/types.d.ts +12 -12
- package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/semanticProjection.js +36 -14
- package/dist/remediate/contractPipeline/semanticProjection.js.map +1 -1
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +27 -8
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/state/itemStatus.d.ts +5 -3
- package/dist/remediate/state/itemStatus.d.ts.map +1 -1
- package/dist/remediate/state/itemStatus.js +7 -3
- package/dist/remediate/state/itemStatus.js.map +1 -1
- package/dist/remediate/state/types.d.ts +7 -2
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js +7 -0
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/dispatch.d.ts +109 -0
- package/dist/remediate/steps/dispatch.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch.js +342 -53
- package/dist/remediate/steps/dispatch.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +215 -21
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/prompts.d.ts +12 -0
- package/dist/remediate/steps/prompts.d.ts.map +1 -1
- package/dist/remediate/steps/prompts.js +61 -1
- package/dist/remediate/steps/prompts.js.map +1 -1
- package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
- package/dist/remediate/steps/rollingSession.js +52 -5
- package/dist/remediate/steps/rollingSession.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +10 -1
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +5 -2
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.js +25 -7
- package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
- package/dist/shared/engine/obligationEngine.d.ts +16 -5
- package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
- package/dist/shared/engine/obligationEngine.js +21 -0
- package/dist/shared/engine/obligationEngine.js.map +1 -1
- package/dist/shared/index.d.ts +4 -2
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +4 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.js +5 -1
- package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
- package/dist/shared/types/intentCheckpoint.d.ts +151 -34
- package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
- package/dist/shared/types/intentCheckpoint.js +113 -1
- package/dist/shared/types/intentCheckpoint.js.map +1 -1
- package/dist/shared/types/remediationOutcome.d.ts +203 -24
- package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
- package/dist/shared/types/remediationOutcome.js +65 -1
- package/dist/shared/types/remediationOutcome.js.map +1 -1
- package/dist/shared/types/sessionConfig.d.ts +3 -1
- package/dist/shared/types/sessionConfig.d.ts.map +1 -1
- package/dist/shared/types/sessionConfig.js +2 -0
- package/dist/shared/types/sessionConfig.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, rename } from "node:fs/promises";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
3
|
-
import { join, relative, dirname, resolve } from "node:path";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { join, relative, dirname, resolve, isAbsolute } from "node:path";
|
|
4
4
|
import { OwnershipRegistry } from "../dispatch/ownershipRegistry.js";
|
|
5
5
|
import { routeAmendmentRequest } from "../dispatch/amendmentClaim.js";
|
|
6
6
|
import { toBlockId, fromBlockId } from "../contractPipeline/idRegistry.js";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { StateStore } from "../state/store.js";
|
|
9
|
-
import { REMEDIATION_STEP, } from "../state/types.js";
|
|
9
|
+
import { REMEDIATION_STEP, isClarificationCategory, } from "../state/types.js";
|
|
10
10
|
import { resolveWindowsShimSpawnCommand } from "audit-tools/shared";
|
|
11
11
|
import { AGENT_FEEDBACK_FILENAME, readJsonFile, readOptionalJsonFile, writeJsonFile, writeTextFile, withFsRetry, formatValidationIssues, detectRepoConventions, formatRepoConventions, toPromptPathToken, estimateTokensFromBytes, buildQuotaSource, severityRank, findingNeedsVerificationBeforeFix, compareTier, mostCapableTier, normalizeRepoPath, hasConfiguredOpenAiCompatible, } from "audit-tools/shared";
|
|
12
12
|
import { validateImplementWorkerResult } from "../validation/artifacts.js";
|
|
@@ -371,6 +371,40 @@ export function createWorktree(root, worktreePath, branchName) {
|
|
|
371
371
|
throw new Error(`git worktree add failed (exit ${result.status ?? "unknown"}):\n${stderr || stdout}`);
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Materialize into a fresh worktree any of the node's declared target paths that
|
|
376
|
+
* exist in the main tree but are absent from the worktree — i.e. git-untracked or
|
|
377
|
+
* gitignored files that `git worktree add HEAD` does not bring over. Without this
|
|
378
|
+
* a node whose scope names an untracked config file (the dogfood hit
|
|
379
|
+
* `opencode.json` and an uncommitted `.gemini/commands/*.toml`) cannot see its own
|
|
380
|
+
* target, so the edit silently no-ops. The "absent in worktree" test is the
|
|
381
|
+
* discriminator: a tracked path is already materialized from HEAD, so only the
|
|
382
|
+
* genuinely-missing untracked/ignored declarations are copied — a tracked-but-dirty
|
|
383
|
+
* file keeps its clean-from-HEAD worktree content and is never clobbered. Paths are
|
|
384
|
+
* repo-relative (the declared scope contract); absolute/escaping paths are skipped.
|
|
385
|
+
* Best-effort: a copy failure must not abort the dispatch (logged, not thrown).
|
|
386
|
+
*/
|
|
387
|
+
export function seedUntrackedDeclaredPaths(root, worktreeRoot, declaredPaths) {
|
|
388
|
+
for (const rel of new Set(declaredPaths)) {
|
|
389
|
+
if (!rel || isAbsolute(rel))
|
|
390
|
+
continue;
|
|
391
|
+
// Reject paths that escape the root (defence-in-depth; declared scope is
|
|
392
|
+
// repo-relative and never `..`-prefixed in practice).
|
|
393
|
+
const dst = join(worktreeRoot, rel);
|
|
394
|
+
const src = join(root, rel);
|
|
395
|
+
if (relative(worktreeRoot, dst).startsWith(".."))
|
|
396
|
+
continue;
|
|
397
|
+
if (!existsSync(src) || existsSync(dst))
|
|
398
|
+
continue;
|
|
399
|
+
try {
|
|
400
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
401
|
+
cpSync(src, dst, { recursive: true });
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
process.stderr.write(`[remediate-code] worktree seed: could not copy untracked declared path ${rel}: ${err.message}\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
374
408
|
/** Remove a git worktree. Best-effort: logs but does not throw on failure. */
|
|
375
409
|
export function removeWorktree(root, worktreePath) {
|
|
376
410
|
const result = spawnSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: root, encoding: "utf8", shell: false });
|
|
@@ -476,6 +510,116 @@ export function mergeWorktree(root, worktreePath, branchName) {
|
|
|
476
510
|
export function worktreePath(root, blockId, runId) {
|
|
477
511
|
return join(root, ".audit-tools", "worktrees", `remediate-${blockId}-${runId}`);
|
|
478
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Deterministic name of the dedicated remediation branch for a run. Derived from
|
|
515
|
+
* the stable run id (= the plan id, constant for the whole remediation) so every
|
|
516
|
+
* wave and the final report resolve the SAME branch without persisting it. Ref-safe:
|
|
517
|
+
* any character outside [A-Za-z0-9._-] collapses to '-'. Distinct from the per-node
|
|
518
|
+
* worktree branches (`remediate-<blockId>-<runId>`) — this uses a `remediation/` ref
|
|
519
|
+
* namespace so the two never collide.
|
|
520
|
+
*/
|
|
521
|
+
function refSafeSegment(s, fallback) {
|
|
522
|
+
return (s
|
|
523
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
524
|
+
.replace(/\.{2,}/g, ".") // ".." is invalid in a git ref name
|
|
525
|
+
.replace(/^[-.]+|[-.]+$/g, "") || fallback);
|
|
526
|
+
}
|
|
527
|
+
export function remediationBranchName(runId) {
|
|
528
|
+
return `remediation/${refSafeSegment(runId, "run")}`;
|
|
529
|
+
}
|
|
530
|
+
/** Durable ref under which a failed-but-committed node's commit is preserved. */
|
|
531
|
+
function quarantineRef(runId, blockId) {
|
|
532
|
+
return `refs/remediation-quarantine/${refSafeSegment(runId, "run")}/${refSafeSegment(blockId, "node")}`;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Preserve a failed-but-committed node's work so it can never be lost. A node that
|
|
536
|
+
* committed real edits to its worktree branch but then failed verify / the
|
|
537
|
+
* write-scope gate / the cherry-pick is about to have its worktree removed and (on
|
|
538
|
+
* the next re-dispatch) its branch force-deleted — orphaning the commit. The dogfood
|
|
539
|
+
* lost a verified fix exactly this way (the worktree was pruned before recovery).
|
|
540
|
+
* Point a durable ref at the branch tip: a ref under refs/remediation-quarantine/
|
|
541
|
+
* survives `git branch -D` and `git worktree prune`, so the work stays reachable for
|
|
542
|
+
* a manual `git cherry-pick`. Best-effort; returns the ref + commit, or null.
|
|
543
|
+
*/
|
|
544
|
+
export function quarantineFailedNodeCommit(root, branch, runId, blockId) {
|
|
545
|
+
const rev = spawnSync("git", ["rev-parse", "--verify", "--quiet", `${branch}^{commit}`], {
|
|
546
|
+
cwd: root,
|
|
547
|
+
encoding: "utf8",
|
|
548
|
+
shell: false,
|
|
549
|
+
});
|
|
550
|
+
if (rev.status !== 0)
|
|
551
|
+
return null;
|
|
552
|
+
const commit = (rev.stdout ?? "").trim();
|
|
553
|
+
const ref = quarantineRef(runId, blockId);
|
|
554
|
+
const upd = spawnSync("git", ["update-ref", ref, commit], {
|
|
555
|
+
cwd: root,
|
|
556
|
+
encoding: "utf8",
|
|
557
|
+
shell: false,
|
|
558
|
+
});
|
|
559
|
+
if (upd.status !== 0) {
|
|
560
|
+
process.stderr.write(`[remediate-code] could not quarantine ${branch}: ${(upd.stderr ?? "").trim()}\n`);
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
process.stderr.write(`[remediate-code] preserved failed node ${blockId} commit ${commit.slice(0, 8)} at ${ref} for recovery\n`);
|
|
564
|
+
return { ref, commit };
|
|
565
|
+
}
|
|
566
|
+
/** Clear a node's quarantine ref (e.g. once a later re-dispatch landed successfully). Best-effort. */
|
|
567
|
+
export function clearQuarantinedCommit(root, runId, blockId) {
|
|
568
|
+
spawnSync("git", ["update-ref", "-d", quarantineRef(runId, blockId)], {
|
|
569
|
+
cwd: root,
|
|
570
|
+
encoding: "utf8",
|
|
571
|
+
shell: false,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/** Quarantined failed-node commits still preserved for a run, for recovery surfacing in the report. */
|
|
575
|
+
export function listQuarantinedCommits(root, runId) {
|
|
576
|
+
const prefix = `refs/remediation-quarantine/${refSafeSegment(runId, "run")}/`;
|
|
577
|
+
const res = spawnSync("git", ["for-each-ref", "--format=%(refname) %(objectname)", prefix], {
|
|
578
|
+
cwd: root,
|
|
579
|
+
encoding: "utf8",
|
|
580
|
+
shell: false,
|
|
581
|
+
});
|
|
582
|
+
if (res.status !== 0 || !res.stdout)
|
|
583
|
+
return [];
|
|
584
|
+
const out = [];
|
|
585
|
+
for (const line of res.stdout.split("\n")) {
|
|
586
|
+
const [ref, commit] = line.trim().split(/\s+/);
|
|
587
|
+
if (!ref || !commit)
|
|
588
|
+
continue;
|
|
589
|
+
out.push({ block: ref.slice(prefix.length), ref, commit });
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Ensure the main checkout is on the dedicated remediation branch BEFORE any node
|
|
595
|
+
* commit is cherry-picked, so accepted work lands there and the user's base branch
|
|
596
|
+
* is NEVER modified — the run leaves a feature branch for review (it does not merge
|
|
597
|
+
* back). Idempotent across waves: creates the branch from the current HEAD (the base)
|
|
598
|
+
* the first time, checks it out on later waves. Best-effort on a non-git root (the
|
|
599
|
+
* worktree dispatch flow can't run there anyway): returns null without throwing so
|
|
600
|
+
* non-git callers/tests are unaffected. Returns the branch name on success.
|
|
601
|
+
*/
|
|
602
|
+
export function ensureRemediationBranchCheckedOut(root, runId) {
|
|
603
|
+
const top = gitTopLevel(root);
|
|
604
|
+
if (top === null || canonicalPathKey(top) !== canonicalPathKey(root))
|
|
605
|
+
return null;
|
|
606
|
+
const branch = remediationBranchName(runId);
|
|
607
|
+
const current = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
608
|
+
cwd: root,
|
|
609
|
+
encoding: "utf8",
|
|
610
|
+
shell: false,
|
|
611
|
+
});
|
|
612
|
+
if (current.status === 0 && (current.stdout ?? "").trim() === branch)
|
|
613
|
+
return branch;
|
|
614
|
+
const args = gitBranchExists(root, branch) ? ["checkout", branch] : ["checkout", "-b", branch];
|
|
615
|
+
const co = spawnSync("git", args, { cwd: root, encoding: "utf8", shell: false });
|
|
616
|
+
if (co.status !== 0) {
|
|
617
|
+
process.stderr.write(`[remediate-code] could not switch to remediation branch ${branch}: ${(co.stderr ?? "").trim()}\n`);
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
process.stderr.write(`[remediate-code] remediation changes land on branch ${branch} (base branch left untouched)\n`);
|
|
621
|
+
return branch;
|
|
622
|
+
}
|
|
479
623
|
/**
|
|
480
624
|
* Stage and commit all of a worktree's edits onto its branch. The TOOL owns this
|
|
481
625
|
* commit (never the worker/host) so that the branch has a real commit for two
|
|
@@ -572,7 +716,7 @@ export function acceptNodeWorktree(params) {
|
|
|
572
716
|
if (commit.error) {
|
|
573
717
|
// Could not commit the worker's edits → cannot safely land; drop it.
|
|
574
718
|
removeWorktree(root, wt);
|
|
575
|
-
return { outcome: "error", verifyPassed, merged };
|
|
719
|
+
return { outcome: "error", verifyPassed, merged, diagnostic: commit.error };
|
|
576
720
|
}
|
|
577
721
|
if (!commit.committed) {
|
|
578
722
|
// Worker reported success but made no tracked edits — nothing to verify or merge.
|
|
@@ -586,14 +730,48 @@ export function acceptNodeWorktree(params) {
|
|
|
586
730
|
verifyPassed = verify.passed;
|
|
587
731
|
if (!verify.passed) {
|
|
588
732
|
// Verify failed: do not merge; drop the worktree so the main tree stays clean.
|
|
733
|
+
// The node DID commit real edits, so preserve them under a durable quarantine
|
|
734
|
+
// ref before the worktree/branch go away — a tool-verify false-negative must
|
|
735
|
+
// not destroy a good fix (the dogfood lost one this way). Carry the failing
|
|
736
|
+
// command + output so triage isn't blind on outcome:error.
|
|
737
|
+
quarantineFailedNodeCommit(root, branch, runId, blockId);
|
|
589
738
|
removeWorktree(root, wt);
|
|
590
|
-
return { outcome: "error", verifyPassed, merged };
|
|
739
|
+
return { outcome: "error", verifyPassed, merged, diagnostic: verify.output };
|
|
740
|
+
}
|
|
741
|
+
// Write-scope gate (OBL-DS-06), BEFORE the cherry-pick: an out-of-scope or
|
|
742
|
+
// seam-conflicting edit must never land in the main tree, so it is adjudicated
|
|
743
|
+
// against the branch's git diff (the ground truth) here rather than reported
|
|
744
|
+
// after `mergeWorktree` already merged it. A worker that legitimately needed a
|
|
745
|
+
// file outside its declared scope declares it via `amended_files`; an unowned
|
|
746
|
+
// amendment widens the effective scope, while one owned by another block blocks.
|
|
747
|
+
if (params.scope) {
|
|
748
|
+
const decision = enforceAcceptWriteScope({
|
|
749
|
+
root,
|
|
750
|
+
branch,
|
|
751
|
+
blockId,
|
|
752
|
+
allBlockScopes: params.scope.allBlockScopes,
|
|
753
|
+
amendedFiles: params.scope.amendedFiles,
|
|
754
|
+
});
|
|
755
|
+
if (decision.blocked) {
|
|
756
|
+
// Scope-blocked but the node committed real work — preserve it for recovery.
|
|
757
|
+
quarantineFailedNodeCommit(root, branch, runId, blockId);
|
|
758
|
+
removeWorktree(root, wt);
|
|
759
|
+
return { outcome: "error", verifyPassed, merged: false, diagnostic: decision.reason };
|
|
760
|
+
}
|
|
591
761
|
}
|
|
592
762
|
// mergeWorktree cherry-picks the verified branch and removes the worktree (on
|
|
593
763
|
// success AND on conflict-abort), so no explicit cleanup is needed afterwards.
|
|
594
764
|
const mergeRes = mergeWorktree(root, wt, branch);
|
|
595
765
|
merged = mergeRes.success;
|
|
596
|
-
|
|
766
|
+
if (!mergeRes.success) {
|
|
767
|
+
// Cherry-pick conflict: the committed work would otherwise be orphaned — preserve it.
|
|
768
|
+
quarantineFailedNodeCommit(root, branch, runId, blockId);
|
|
769
|
+
return { outcome: "error", verifyPassed, merged, diagnostic: mergeRes.error };
|
|
770
|
+
}
|
|
771
|
+
// Landed successfully: clear any quarantine ref left by a prior failed attempt
|
|
772
|
+
// for this node so the recovery report lists only genuinely-unrecovered work.
|
|
773
|
+
clearQuarantinedCommit(root, runId, blockId);
|
|
774
|
+
return { outcome: "success", verifyPassed, merged };
|
|
597
775
|
}
|
|
598
776
|
/**
|
|
599
777
|
* Sidecar path for a node's tool-owned accept (verify/merge) outcome. Written by
|
|
@@ -619,6 +797,8 @@ export async function recordNodeAcceptOutcome(artifactsDir, runId, blockId, resu
|
|
|
619
797
|
outcome: result.outcome,
|
|
620
798
|
verify_passed: result.verifyPassed,
|
|
621
799
|
merged: result.merged,
|
|
800
|
+
// Only present on a failure outcome; gives triage the failing command + output.
|
|
801
|
+
...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}),
|
|
622
802
|
});
|
|
623
803
|
}
|
|
624
804
|
/** Load a node's recorded accept outcome, or null when none was written. */
|
|
@@ -626,7 +806,12 @@ export async function loadNodeAcceptOutcome(artifactsDir, runId, blockId) {
|
|
|
626
806
|
const raw = await readOptionalJsonFile(nodeAcceptOutcomePath(artifactsDir, runId, blockId));
|
|
627
807
|
if (!raw)
|
|
628
808
|
return null;
|
|
629
|
-
return {
|
|
809
|
+
return {
|
|
810
|
+
outcome: raw.outcome,
|
|
811
|
+
verifyPassed: raw.verify_passed,
|
|
812
|
+
merged: raw.merged,
|
|
813
|
+
...(raw.diagnostic !== undefined ? { diagnostic: raw.diagnostic } : {}),
|
|
814
|
+
};
|
|
630
815
|
}
|
|
631
816
|
/** Read the promoted DAG-node overlay fields off a Finding (all optional). */
|
|
632
817
|
function nodeFieldsOf(finding) {
|
|
@@ -1183,7 +1368,10 @@ function implementPrompt(block, state, resultPath, conventions, repoRoot, feedba
|
|
|
1183
1368
|
return [];
|
|
1184
1369
|
// item_spec may be pre-populated from the plan DAG node or absent;
|
|
1185
1370
|
// either way the implementer receives finding context directly.
|
|
1186
|
-
|
|
1371
|
+
// clarification_context carries the user's answer when this item was re-opened
|
|
1372
|
+
// from a clarification round (up-front gate or mid-run) — thread it through so
|
|
1373
|
+
// the retry acts on the decided scope, not the original ambiguity.
|
|
1374
|
+
return [{ finding, spec: item.item_spec, clarification: item.clarification_context }];
|
|
1187
1375
|
});
|
|
1188
1376
|
// When a worktreeRoot is supplied, the worker operates in the worktree, not
|
|
1189
1377
|
// the main repo root. Source file paths are prefixed with the worktree root.
|
|
@@ -1223,12 +1411,12 @@ Set the shell/tool workdir to the repository root when running commands; do not
|
|
|
1223
1411
|
## Items
|
|
1224
1412
|
|
|
1225
1413
|
${items
|
|
1226
|
-
.map(({ finding, spec }) => `
|
|
1414
|
+
.map(({ finding, spec, clarification }) => `
|
|
1227
1415
|
### ${finding.id} - ${finding.title}
|
|
1228
1416
|
|
|
1229
1417
|
- Files: ${itemReadFiles(finding, spec).map(resolveFilePath).join(", ")}
|
|
1230
1418
|
- Summary: ${finding.summary}
|
|
1231
|
-
${groundingVerificationBullet(finding)}
|
|
1419
|
+
${clarification ? `- Clarified scope (decided with the user — act on THIS): ${clarification}\n` : ""}${groundingVerificationBullet(finding)}
|
|
1232
1420
|
${spec ? `- Concrete change: ${spec.concrete_change}
|
|
1233
1421
|
- Tests to write: ${spec.tests_to_write
|
|
1234
1422
|
.map((test) => `${test.name}: ${test.assertions.join("; ")}`)
|
|
@@ -1279,8 +1467,15 @@ ${items
|
|
|
1279
1467
|
}
|
|
1280
1468
|
\`\`\`
|
|
1281
1469
|
|
|
1282
|
-
For an item you cannot safely finish
|
|
1283
|
-
|
|
1470
|
+
For an item you cannot safely finish because of an EXECUTION failure (a test
|
|
1471
|
+
won't pass, a build breaks, the change is infeasible), set \`status\` to
|
|
1472
|
+
\`blocked\` and include \`failure_reason\`. If instead you are stuck on a SCOPING
|
|
1473
|
+
or JUDGMENT question — how far the fix should reach, which of several valid
|
|
1474
|
+
behaviors is intended, or whether the issue is real — do NOT guess and do NOT
|
|
1475
|
+
block: set \`status\` to \`needs_clarification\` and put the question in
|
|
1476
|
+
\`clarification_question\` (optionally \`clarification_category\`). It is routed to
|
|
1477
|
+
the user as a real question, then re-dispatched with the answer. Stop after
|
|
1478
|
+
writing the result JSON.
|
|
1284
1479
|
|
|
1285
1480
|
## File access
|
|
1286
1481
|
|
|
@@ -1336,6 +1531,13 @@ export async function prepareImplementDispatch(options, runId, onlyBlockId, wave
|
|
|
1336
1531
|
}
|
|
1337
1532
|
return false;
|
|
1338
1533
|
});
|
|
1534
|
+
// Before any node is dispatched (and therefore before any accepted commit is
|
|
1535
|
+
// cherry-picked into the main tree), switch the main checkout onto the dedicated
|
|
1536
|
+
// remediation branch so all landed work accumulates there and the base branch is
|
|
1537
|
+
// never modified. Idempotent across waves; only when there is work to land.
|
|
1538
|
+
if (candidateBlocks.length > 0 && options.root) {
|
|
1539
|
+
ensureRemediationBranchCheckedOut(options.root, runId);
|
|
1540
|
+
}
|
|
1339
1541
|
// Walk the repo for test files ONCE per dispatch (not once per block) and cache
|
|
1340
1542
|
// their contents; collectReferencingTests then matches in memory.
|
|
1341
1543
|
const testIndex = buildTestFileIndex(options.root);
|
|
@@ -1554,6 +1756,68 @@ export function enforceWriteScope(declaredWritePaths, edited, root) {
|
|
|
1554
1756
|
`amended_files set is not trusted for this gate.`,
|
|
1555
1757
|
};
|
|
1556
1758
|
}
|
|
1759
|
+
/** Each block's declared write scope from a dispatch plan — the seed for the
|
|
1760
|
+
* accept-time write-scope gate's ownership registry (so an amended path owned by
|
|
1761
|
+
* a sibling block is recognised as a seam conflict, not silently granted). */
|
|
1762
|
+
export function blockScopesFromPlan(plan) {
|
|
1763
|
+
return plan.items.flatMap((item) => item.block_id && item.access
|
|
1764
|
+
? [{ block_id: item.block_id, write_paths: item.access.write_paths }]
|
|
1765
|
+
: []);
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* A block's declared target paths (write ∪ read) from the persisted dispatch plan
|
|
1769
|
+
* — the single source of the scope the worker actually received (same authority
|
|
1770
|
+
* the accept-time write-scope gate reads). Used to seed untracked declared targets
|
|
1771
|
+
* into a fresh worktree (see {@link seedUntrackedDeclaredPaths}).
|
|
1772
|
+
*/
|
|
1773
|
+
export function declaredPathsFromPlan(plan, blockId) {
|
|
1774
|
+
const item = plan.items.find((i) => i.block_id === blockId);
|
|
1775
|
+
if (!item?.access)
|
|
1776
|
+
return [];
|
|
1777
|
+
return [...(item.access.write_paths ?? []), ...(item.access.read_paths ?? [])];
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Accept-time write-scope gate (OBL-DS-06), run from `acceptNodeWorktree` AFTER
|
|
1781
|
+
* the verify and BEFORE the cherry-pick so a violation PREVENTS the merge rather
|
|
1782
|
+
* than being reported once the edit already landed in main. It adjudicates the
|
|
1783
|
+
* worker's self-reported `amended_files` against all blocks' declared scopes via
|
|
1784
|
+
* an ephemeral `OwnershipRegistry` seeded from `allBlockScopes`: an unowned
|
|
1785
|
+
* amended path is granted and widens this node's effective scope (the surfaced
|
|
1786
|
+
* amend path — a too-narrow declared scope no longer blocks a correct fix); a path
|
|
1787
|
+
* owned by another block is a seam conflict that blocks. The gate then diffs the
|
|
1788
|
+
* worktree branch (the git ground truth, never the self-report) against the
|
|
1789
|
+
* effective scope. Cross-sibling contention on a file two live nodes both amend is
|
|
1790
|
+
* left to the merge-time lost-update detector (`detectOverlappingEdits`), which
|
|
1791
|
+
* sees the full set of merged blocks a single accept cannot.
|
|
1792
|
+
*/
|
|
1793
|
+
export function enforceAcceptWriteScope(params) {
|
|
1794
|
+
const { root, branch, blockId, allBlockScopes, amendedFiles } = params;
|
|
1795
|
+
const registry = new OwnershipRegistry();
|
|
1796
|
+
registry.initialize(allBlockScopes.map((b) => ({ node_id: b.block_id, write_paths: b.write_paths })));
|
|
1797
|
+
if (amendedFiles.length > 0) {
|
|
1798
|
+
const { seam_routed } = routeAmendmentRequest(registry, blockId, amendedFiles);
|
|
1799
|
+
if (seam_routed.length > 0) {
|
|
1800
|
+
const detail = seam_routed
|
|
1801
|
+
.map((r) => {
|
|
1802
|
+
const reason = r.reason;
|
|
1803
|
+
if (reason.outcome === "owned")
|
|
1804
|
+
return `${r.path} owned by ${reason.owner_node_id}`;
|
|
1805
|
+
if (reason.outcome === "contended") {
|
|
1806
|
+
return `${r.path} contended by ${reason.sibling_node_id}`;
|
|
1807
|
+
}
|
|
1808
|
+
return r.path;
|
|
1809
|
+
})
|
|
1810
|
+
.join("; ");
|
|
1811
|
+
return {
|
|
1812
|
+
blocked: true,
|
|
1813
|
+
reason: `Worker amended files owned by another block (seam conflict): ${detail}. ` +
|
|
1814
|
+
`Resolve via the seam protocol before this node can land.`,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const effective = registry.getScope(blockId);
|
|
1819
|
+
return enforceWriteScope(effective, gitEditedFilesForBranch(root, branch), root);
|
|
1820
|
+
}
|
|
1557
1821
|
// ---------------------------------------------------------------------------
|
|
1558
1822
|
// Merge-seam: obligation-id → node remap + multi-entry collapse (tolerance)
|
|
1559
1823
|
// ---------------------------------------------------------------------------
|
|
@@ -1634,18 +1898,28 @@ export function collapseItemResults(itemResults, aliasMap, knownFindingIds) {
|
|
|
1634
1898
|
byFinding.set(targetId, normalized);
|
|
1635
1899
|
continue;
|
|
1636
1900
|
}
|
|
1637
|
-
// Collapse: blocked
|
|
1901
|
+
// Collapse precedence: blocked > needs_clarification > resolved >
|
|
1902
|
+
// resolved_no_change. A hard failure dominates an unanswered scoping question,
|
|
1903
|
+
// which dominates an actual change, which dominates a no-change claim (a
|
|
1904
|
+
// no-change claim only survives if every entry agreed nothing changed). Merge
|
|
1905
|
+
// evidence; keep first failure_reason / clarification question.
|
|
1638
1906
|
const mergedEvidence = [
|
|
1639
1907
|
...new Set([...(existing.evidence ?? []), ...(normalized.evidence ?? [])]),
|
|
1640
1908
|
];
|
|
1641
1909
|
const status = existing.status === "blocked" || normalized.status === "blocked"
|
|
1642
1910
|
? "blocked"
|
|
1643
|
-
: "
|
|
1911
|
+
: existing.status === "needs_clarification" || normalized.status === "needs_clarification"
|
|
1912
|
+
? "needs_clarification"
|
|
1913
|
+
: existing.status === "resolved" || normalized.status === "resolved"
|
|
1914
|
+
? "resolved"
|
|
1915
|
+
: "resolved_no_change";
|
|
1644
1916
|
byFinding.set(targetId, {
|
|
1645
1917
|
finding_id: targetId,
|
|
1646
1918
|
status,
|
|
1647
1919
|
evidence: mergedEvidence.length > 0 ? mergedEvidence : undefined,
|
|
1648
1920
|
failure_reason: existing.failure_reason ?? normalized.failure_reason,
|
|
1921
|
+
clarification_question: existing.clarification_question ?? normalized.clarification_question,
|
|
1922
|
+
clarification_category: existing.clarification_category ?? normalized.clarification_category,
|
|
1649
1923
|
});
|
|
1650
1924
|
}
|
|
1651
1925
|
return { collapsed: [...byFinding.values()], unresolved };
|
|
@@ -1954,9 +2228,11 @@ async function mergeImplementResultsIntoState(options, runId, plan, state) {
|
|
|
1954
2228
|
if (isTerminalStatus(stateItem.status)) {
|
|
1955
2229
|
continue;
|
|
1956
2230
|
}
|
|
1957
|
-
if (itemResult.status === "resolved") {
|
|
2231
|
+
if (itemResult.status === "resolved" || itemResult.status === "resolved_no_change") {
|
|
1958
2232
|
const spec = stateItem.item_spec;
|
|
1959
|
-
|
|
2233
|
+
// The worker's explicit `resolved_no_change` is a no-change signal in its
|
|
2234
|
+
// own right; the spec heuristic is the fallback for a plain `resolved`.
|
|
2235
|
+
const isNoChange = itemResult.status === "resolved_no_change" || specIndicatesNoChange(spec);
|
|
1960
2236
|
if (isNoChange && !hasExecutableEvidence(itemResult.evidence)) {
|
|
1961
2237
|
// No-prose closure: a "verified-already-satisfied" (no-change) claim must
|
|
1962
2238
|
// be backed by an executable assertion (a test/build/check command +
|
|
@@ -1990,6 +2266,31 @@ async function mergeImplementResultsIntoState(options, runId, plan, state) {
|
|
|
1990
2266
|
}
|
|
1991
2267
|
}
|
|
1992
2268
|
}
|
|
2269
|
+
else if (itemResult.status === "needs_clarification") {
|
|
2270
|
+
// Mid-run escape hatch (note 3, part B): the worker hit scoping/judgment
|
|
2271
|
+
// ambiguity. Route it to a clarification round (a real user question), not
|
|
2272
|
+
// to triage's retry/ignore/halt. NOT terminal — the answer re-opens it.
|
|
2273
|
+
stateItem.status = "needs_clarification";
|
|
2274
|
+
const question = itemResult.clarification_question ??
|
|
2275
|
+
itemResult.failure_reason ??
|
|
2276
|
+
"The worker reported unresolved scoping/judgment ambiguity.";
|
|
2277
|
+
stateItem.failure_reason = question;
|
|
2278
|
+
const category = isClarificationCategory(itemResult.clarification_category)
|
|
2279
|
+
? itemResult.clarification_category
|
|
2280
|
+
: "scope_of_fix";
|
|
2281
|
+
const clarifications = state.clarifications ?? [];
|
|
2282
|
+
if (!clarifications.some((c) => c.finding_id === itemResult.finding_id)) {
|
|
2283
|
+
clarifications.push({
|
|
2284
|
+
finding_id: itemResult.finding_id,
|
|
2285
|
+
category,
|
|
2286
|
+
description: question,
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
state.clarifications = clarifications;
|
|
2290
|
+
// The run is paused for the batched clarification round at the single
|
|
2291
|
+
// post-loop status decision below (a needs_clarification item outranks
|
|
2292
|
+
// implementing/triage), so the answer is applied before any more work.
|
|
2293
|
+
}
|
|
1993
2294
|
else {
|
|
1994
2295
|
stateItem.status = "blocked";
|
|
1995
2296
|
markTerminal(stateItem);
|
|
@@ -1997,44 +2298,22 @@ async function mergeImplementResultsIntoState(options, runId, plan, state) {
|
|
|
1997
2298
|
itemResult.failure_reason ?? "Implementation worker blocked.";
|
|
1998
2299
|
}
|
|
1999
2300
|
}
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2004
|
-
//
|
|
2005
|
-
//
|
|
2006
|
-
//
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2301
|
+
// Per-block ACTUAL worktree-branch edits, collected for the post-loop
|
|
2302
|
+
// lost-update / overlapping-edit detection (a file edited by more than one
|
|
2303
|
+
// merged block). The write-scope gate itself is NOT applied here: it runs at
|
|
2304
|
+
// ACCEPT time (`acceptNodeWorktree` → `enforceAcceptWriteScope`), BEFORE the
|
|
2305
|
+
// cherry-pick, so an out-of-scope edit is prevented from landing rather than
|
|
2306
|
+
// reported once already merged — and a node it blocks reaches the merge as
|
|
2307
|
+
// `merged:false`, routed to triage by the merge-state gate below (with the
|
|
2308
|
+
// write-scope reason carried in its diagnostic). A missing branch means the
|
|
2309
|
+
// interim main-tree path was used (no per-worker diff): nothing to collect.
|
|
2009
2310
|
const worktreeBranch = worktreeBranchForBlock(blockId, runId);
|
|
2010
|
-
// Resolve this block's ACTUAL worktree-branch edits ONCE when the branch
|
|
2011
|
-
// exists — reused for the write-scope gate below AND the post-loop
|
|
2012
|
-
// lost-update detection. A missing branch means the interim main-tree path
|
|
2013
|
-
// was used (no per-worker diff): skip both checks for this block.
|
|
2014
2311
|
const branchEdited = gitBranchExists(options.root, worktreeBranch)
|
|
2015
2312
|
? gitEditedFilesForBranch(options.root, worktreeBranch)
|
|
2016
2313
|
: null;
|
|
2017
2314
|
if (branchEdited?.available) {
|
|
2018
2315
|
editedByBlock.push({ block_id: blockId, files: branchEdited.files });
|
|
2019
2316
|
}
|
|
2020
|
-
// Activate the write-scope gate only when this block was actually dispatched
|
|
2021
|
-
// through an isolated worktree (its branch exists). A missing branch means
|
|
2022
|
-
// the interim main-tree path was used — there is no per-worker diff to
|
|
2023
|
-
// enforce against, so the gate is skipped (NOT fail-closed: fail-closed is
|
|
2024
|
-
// for a present repo whose diff genuinely errors).
|
|
2025
|
-
if (resolvedFindingIds.length > 0 && item.access && branchEdited) {
|
|
2026
|
-
const decision = enforceWriteScope(item.access.write_paths, branchEdited, options.root);
|
|
2027
|
-
if (decision.blocked) {
|
|
2028
|
-
for (const findingId of resolvedFindingIds) {
|
|
2029
|
-
const stateItem = state.items[findingId];
|
|
2030
|
-
if (!stateItem)
|
|
2031
|
-
continue;
|
|
2032
|
-
stateItem.status = "blocked";
|
|
2033
|
-
markTerminal(stateItem);
|
|
2034
|
-
stateItem.failure_reason = decision.reason;
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
2317
|
// Merge-state gate (authoritative, OBL-DS-06): a node that self-reported a
|
|
2039
2318
|
// finding "resolved" but whose tool-owned verify/merge did NOT land its edits
|
|
2040
2319
|
// (acceptNodeWorktree returned merged:false — verify failed, a cherry-pick
|
|
@@ -2057,7 +2336,10 @@ async function mergeImplementResultsIntoState(options, runId, plan, state) {
|
|
|
2057
2336
|
`Node ${blockId} reported finding ${findingId} resolved, but its tool-owned ` +
|
|
2058
2337
|
`verify/merge did not land the edits (outcome=${acceptOutcome.outcome}, ` +
|
|
2059
2338
|
`verify_passed=${acceptOutcome.verifyPassed}, merged=false); the fix is not in ` +
|
|
2060
|
-
`the main tree. Routed to triage
|
|
2339
|
+
`the main tree. Routed to triage.` +
|
|
2340
|
+
(acceptOutcome.diagnostic
|
|
2341
|
+
? `\nFailing command output:\n${acceptOutcome.diagnostic}`
|
|
2342
|
+
: "");
|
|
2061
2343
|
}
|
|
2062
2344
|
}
|
|
2063
2345
|
}
|
|
@@ -2187,12 +2469,19 @@ async function mergeImplementResultsIntoState(options, runId, plan, state) {
|
|
|
2187
2469
|
}
|
|
2188
2470
|
process.stderr.write(`[remediate-code] dispatch: merged ${implementResolved} implement result(s), ` +
|
|
2189
2471
|
`${implementRejected} rejected\n`);
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2472
|
+
// A worker that reported needs_clarification (note 3, part B) outranks both
|
|
2473
|
+
// implementing and triage: pause the run for the batched clarification round so
|
|
2474
|
+
// the user's answer is applied before any more work is dispatched or triaged.
|
|
2475
|
+
// Otherwise route back to implementing while pending work remains (later
|
|
2476
|
+
// dependency waves, or blocks deferred this wave because a prerequisite was
|
|
2477
|
+
// still running); else advance to triage.
|
|
2478
|
+
const needsClarification = Object.values(state.items).some((it) => it.status === "needs_clarification");
|
|
2194
2479
|
const moreToImplement = Object.values(state.items).some((it) => it.status === "pending");
|
|
2195
|
-
state.status =
|
|
2480
|
+
state.status = needsClarification
|
|
2481
|
+
? "waiting_for_clarification"
|
|
2482
|
+
: moreToImplement
|
|
2483
|
+
? "implementing"
|
|
2484
|
+
: "triage";
|
|
2196
2485
|
// Single commit: StateStore.mutate writes the returned state once, under the
|
|
2197
2486
|
// lock it already holds (OBL-INV-RSD-02 / OBL-SEAM-RSD-04). No saveState here.
|
|
2198
2487
|
return state;
|