@deftai/directive-core 0.107.0 → 0.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arm the shipped sub-agent heartbeat path (#3730 / #1365 / #2824).
|
|
3
|
+
*
|
|
4
|
+
* `verify:subagent-alive` exits 2 when the scratch directory is missing, which
|
|
5
|
+
* looks like "never started" instead of REDISPATCH_OK. Parents mkdir at
|
|
6
|
+
* worktree create and at pre-dispatch begin so a later missing record is
|
|
7
|
+
* exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { defaultScratchDir } from "../orchestration/subagent-monitor.js";
|
|
12
|
+
/**
|
|
13
|
+
* True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
|
|
14
|
+
* entry (a file for `git worktree add` trees, a directory for the main clone).
|
|
15
|
+
*
|
|
16
|
+
* Arming cannot key off `looksLikeFilesystemTarget` alone: that predicate exists
|
|
17
|
+
* to keep branch refs opaque and so answers false for a bare relative worktree
|
|
18
|
+
* name like `b3730`, leaving the liveness gate at its exit-2 config error for a
|
|
19
|
+
* supported dispatch form. Existence alone is too loose in the other direction —
|
|
20
|
+
* a branch named `docs` resolves onto the real `docs/` tree and would mkdir a
|
|
21
|
+
* stray scratch dir in the source checkout. The `.git` marker separates the two.
|
|
22
|
+
*/
|
|
23
|
+
export function looksLikeWorktreeDir(resolvedPath) {
|
|
24
|
+
const trimmed = resolvedPath.trim();
|
|
25
|
+
if (trimmed.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
return existsSync(join(trimmed, ".git"));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create `<worktree>/.deft-scratch/subagent-status/` when the worktree exists.
|
|
31
|
+
* Returns the directory path, or null when the worktree is not on disk yet
|
|
32
|
+
* (do not mkdir a stray tree for a branch-name target).
|
|
33
|
+
*/
|
|
34
|
+
export function ensureSubagentStatusDir(worktreeRoot) {
|
|
35
|
+
const trimmed = worktreeRoot.trim();
|
|
36
|
+
if (trimmed.length === 0 || !existsSync(trimmed))
|
|
37
|
+
return null;
|
|
38
|
+
const dir = defaultScratchDir(trimmed);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=subagent-status-dir.js.map
|
|
@@ -21,6 +21,9 @@ export declare class WorktreeMapConfigError extends Error {
|
|
|
21
21
|
export declare class WorktreePathEscapeError extends WorktreeMapError {
|
|
22
22
|
name: string;
|
|
23
23
|
}
|
|
24
|
+
export declare class WorktreeRevisionMismatchError extends WorktreeMapError {
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
24
27
|
export type GitRunner = (args: readonly string[], cwd: string) => TextCaptureResult;
|
|
25
28
|
export declare const defaultGitRunner: GitRunner;
|
|
26
29
|
/** Case-normalized comparison key for worktree-path equality. */
|
|
@@ -30,9 +33,20 @@ export interface WorktreeRecord {
|
|
|
30
33
|
readonly worktree_path: string;
|
|
31
34
|
readonly base_branch: string;
|
|
32
35
|
}
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
+
/** One `git worktree list --porcelain` record. `head` is the commit OID or null. */
|
|
37
|
+
export interface WorktreePorcelainEntry {
|
|
38
|
+
readonly branch: string | null;
|
|
39
|
+
readonly head: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
|
|
42
|
+
export declare function parseWorktreePorcelain(text: string): Map<string, WorktreePorcelainEntry>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
|
|
45
|
+
*
|
|
46
|
+
* Registered paths are reused only when their porcelain HEAD OID matches the
|
|
47
|
+
* requested base OID. That comparison is a snapshot at resolution time — not a
|
|
48
|
+
* pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
|
|
49
|
+
*/
|
|
36
50
|
export declare function resolveWorktreeMap(mapping: readonly Record<string, unknown>[], baseBranch: string, createMissing?: boolean, options?: {
|
|
37
51
|
repoRoot?: string;
|
|
38
52
|
git?: GitRunner;
|
package/dist/swarm/worktrees.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, resolve as pathResolve } from "node:path";
|
|
3
3
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
4
|
import { C3_FIELDS } from "./constants.js";
|
|
5
|
+
import { ensureSubagentStatusDir } from "./subagent-status-dir.js";
|
|
5
6
|
import { runText } from "./subprocess.js";
|
|
6
7
|
export class WorktreeMapError extends Error {
|
|
7
8
|
name = "WorktreeMapError";
|
|
@@ -24,6 +25,9 @@ export class WorktreeMapConfigError extends Error {
|
|
|
24
25
|
export class WorktreePathEscapeError extends WorktreeMapError {
|
|
25
26
|
name = "WorktreePathEscapeError";
|
|
26
27
|
}
|
|
28
|
+
export class WorktreeRevisionMismatchError extends WorktreeMapError {
|
|
29
|
+
name = "WorktreeRevisionMismatchError";
|
|
30
|
+
}
|
|
27
31
|
export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
|
|
28
32
|
function resolvePath(raw, repoRoot) {
|
|
29
33
|
const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
|
|
@@ -44,14 +48,15 @@ function assertWorktreePathContained(repoRoot, worktreePath) {
|
|
|
44
48
|
export function compareKey(pathStr) {
|
|
45
49
|
return pathStr.replace(/\\/g, "/").toLowerCase();
|
|
46
50
|
}
|
|
47
|
-
/** Parse `git worktree list --porcelain` into `{compareKey: branch
|
|
51
|
+
/** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
|
|
48
52
|
export function parseWorktreePorcelain(text) {
|
|
49
53
|
const registered = new Map();
|
|
50
54
|
let currentPath = null;
|
|
51
55
|
let currentBranch = null;
|
|
56
|
+
let currentHead = null;
|
|
52
57
|
const flush = () => {
|
|
53
58
|
if (currentPath !== null) {
|
|
54
|
-
registered.set(compareKey(currentPath), currentBranch);
|
|
59
|
+
registered.set(compareKey(currentPath), { branch: currentBranch, head: currentHead });
|
|
55
60
|
}
|
|
56
61
|
};
|
|
57
62
|
for (const line of text.split("\n")) {
|
|
@@ -59,6 +64,11 @@ export function parseWorktreePorcelain(text) {
|
|
|
59
64
|
flush();
|
|
60
65
|
currentPath = pathResolve(line.slice("worktree ".length).trim());
|
|
61
66
|
currentBranch = null;
|
|
67
|
+
currentHead = null;
|
|
68
|
+
}
|
|
69
|
+
else if (line.startsWith("HEAD ")) {
|
|
70
|
+
const oid = line.slice("HEAD ".length).trim().toLowerCase();
|
|
71
|
+
currentHead = oid.length > 0 ? oid : null;
|
|
62
72
|
}
|
|
63
73
|
else if (line.startsWith("branch ")) {
|
|
64
74
|
const ref = line.slice("branch ".length).trim();
|
|
@@ -82,6 +92,24 @@ function gitWorktreeList(repoRoot, git) {
|
|
|
82
92
|
}
|
|
83
93
|
return parseWorktreePorcelain(proc.stdout);
|
|
84
94
|
}
|
|
95
|
+
function resolveCommitOid(repoRoot, revision, git) {
|
|
96
|
+
let proc;
|
|
97
|
+
try {
|
|
98
|
+
proc = git(["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`], repoRoot);
|
|
99
|
+
}
|
|
100
|
+
catch (exc) {
|
|
101
|
+
throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot}: ${String(exc)}`);
|
|
102
|
+
}
|
|
103
|
+
if (proc.returncode !== 0) {
|
|
104
|
+
throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot} ` +
|
|
105
|
+
`(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
|
|
106
|
+
}
|
|
107
|
+
const oid = (proc.stdout.trim().split(/\s+/)[0] ?? "").toLowerCase();
|
|
108
|
+
if (!/^[0-9a-f]{40,64}$/.test(oid)) {
|
|
109
|
+
throw new WorktreeMapConfigError(`git rev-parse for base '${revision}' did not return a commit OID: ${JSON.stringify(oid)}`);
|
|
110
|
+
}
|
|
111
|
+
return oid;
|
|
112
|
+
}
|
|
85
113
|
function createWorktree(repoRoot, worktreePath, baseBranch, git) {
|
|
86
114
|
mkdirSync(pathResolve(worktreePath, ".."), { recursive: true });
|
|
87
115
|
let proc;
|
|
@@ -96,7 +124,13 @@ function createWorktree(repoRoot, worktreePath, baseBranch, git) {
|
|
|
96
124
|
`(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
|
|
97
125
|
}
|
|
98
126
|
}
|
|
99
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
|
|
129
|
+
*
|
|
130
|
+
* Registered paths are reused only when their porcelain HEAD OID matches the
|
|
131
|
+
* requested base OID. That comparison is a snapshot at resolution time — not a
|
|
132
|
+
* pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
|
|
133
|
+
*/
|
|
100
134
|
export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, options = {}) {
|
|
101
135
|
if (!Array.isArray(mapping)) {
|
|
102
136
|
throw new WorktreeMapConfigError(`worktree map must be a list of records, got ${typeof mapping}`);
|
|
@@ -154,14 +188,24 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
|
|
|
154
188
|
});
|
|
155
189
|
}
|
|
156
190
|
const registered = gitWorktreeList(root, git);
|
|
191
|
+
const requestedOid = resolveCommitOid(root, trimmedBase, git);
|
|
157
192
|
for (const entry of resolved) {
|
|
158
|
-
|
|
193
|
+
const existing = registered.get(entry._key);
|
|
194
|
+
if (existing !== undefined) {
|
|
195
|
+
const actualOid = existing.head ?? "";
|
|
196
|
+
if (actualOid !== requestedOid) {
|
|
197
|
+
throw new WorktreeRevisionMismatchError(`registered worktree '${entry.worktree_path}' HEAD OID is ${actualOid || "(missing)"} ` +
|
|
198
|
+
`but requested base '${trimmedBase}' resolves to ${requestedOid}; ` +
|
|
199
|
+
`this is a snapshot check at resolution time, not a pin on the worker's start revision`);
|
|
200
|
+
}
|
|
201
|
+
ensureSubagentStatusDir(entry._abs);
|
|
159
202
|
continue;
|
|
160
203
|
}
|
|
161
204
|
if (!createMissing) {
|
|
162
205
|
throw new MissingWorktreeError(`story '${entry.story_id}' maps to '${entry.worktree_path}' which is not a registered git worktree and create_missing is disabled`);
|
|
163
206
|
}
|
|
164
207
|
createWorktree(root, entry._abs, trimmedBase, git);
|
|
208
|
+
ensureSubagentStatusDir(entry._abs);
|
|
165
209
|
}
|
|
166
210
|
return resolved.map(({ story_id, worktree_path, base_branch }) => ({
|
|
167
211
|
story_id,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { containedRemove, containedWrite } from "../../fs/contained-write.js";
|
|
3
5
|
import { defaultGitRunner as swarmGitRunner, } from "../../swarm/worktrees.js";
|
|
4
6
|
import { evaluatorWorktreePath } from "./paths.js";
|
|
5
7
|
export class EvaluatorWorktreeError extends Error {
|
|
@@ -27,17 +29,162 @@ function forceDeleteWorktreeDir(worktreePath) {
|
|
|
27
29
|
}
|
|
28
30
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
29
31
|
}
|
|
32
|
+
const caseInsensitiveDirCache = new Map();
|
|
33
|
+
function slashResolve(path) {
|
|
34
|
+
return resolve(path).replace(/\\/g, "/");
|
|
35
|
+
}
|
|
36
|
+
function directoryIgnoresCase(dir) {
|
|
37
|
+
let existing = resolve(dir);
|
|
38
|
+
while (!existsSync(existing)) {
|
|
39
|
+
const parent = dirname(existing);
|
|
40
|
+
if (parent === existing) {
|
|
41
|
+
return process.platform === "win32";
|
|
42
|
+
}
|
|
43
|
+
existing = parent;
|
|
44
|
+
}
|
|
45
|
+
const cached = caseInsensitiveDirCache.get(existing);
|
|
46
|
+
if (cached !== undefined) {
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
const tag = randomBytes(6).toString("hex");
|
|
50
|
+
const lower = join(existing, `.deft-cs-${tag}a`);
|
|
51
|
+
const upper = join(existing, `.deft-cs-${tag}A`);
|
|
52
|
+
let ignores = false;
|
|
53
|
+
let probed = false;
|
|
54
|
+
try {
|
|
55
|
+
containedWrite({
|
|
56
|
+
root: existing,
|
|
57
|
+
target: lower,
|
|
58
|
+
data: "",
|
|
59
|
+
mode: "create",
|
|
60
|
+
mkdir: false,
|
|
61
|
+
mutation: false,
|
|
62
|
+
});
|
|
63
|
+
ignores = existsSync(upper);
|
|
64
|
+
probed = true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Fail closed: do not fold unless the probe proved case-insensitivity.
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
try {
|
|
71
|
+
containedRemove({ root: existing, target: lower, mutation: false });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
containedRemove({ root: existing, target: upper, mutation: false });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (probed) {
|
|
84
|
+
caseInsensitiveDirCache.set(existing, ignores);
|
|
85
|
+
}
|
|
86
|
+
return ignores;
|
|
87
|
+
}
|
|
88
|
+
function canonicalizeWorktreePath(path) {
|
|
89
|
+
const resolved = slashResolve(path);
|
|
90
|
+
let existing = "";
|
|
91
|
+
if (existsSync(path)) {
|
|
92
|
+
existing = path;
|
|
93
|
+
}
|
|
94
|
+
else if (existsSync(resolved)) {
|
|
95
|
+
existing = resolved;
|
|
96
|
+
}
|
|
97
|
+
if (existing.length > 0) {
|
|
98
|
+
try {
|
|
99
|
+
return realpathSync.native(existing).replace(/\\/g, "/");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Path vanished between exists and realpath.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const parent = dirname(resolved);
|
|
106
|
+
const base = basename(resolved);
|
|
107
|
+
let parentCanon = slashResolve(parent);
|
|
108
|
+
try {
|
|
109
|
+
if (existsSync(parent)) {
|
|
110
|
+
parentCanon = realpathSync.native(parent).replace(/\\/g, "/");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Keep slash-resolved parent.
|
|
115
|
+
}
|
|
116
|
+
const leaf = directoryIgnoresCase(parent) ? base.toLowerCase() : base;
|
|
117
|
+
return `${parentCanon}/${leaf}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Unregister one missing worktree by deleting only its `$GIT_DIR/worktrees/<id>`
|
|
121
|
+
* admin directory. Containment root is that worktrees dir (the git common dir
|
|
122
|
+
* may sit outside a linked-worktree projectRoot). `git worktree prune` has no
|
|
123
|
+
* path argument and operates on every registration, including concurrent
|
|
124
|
+
* agents' reflogs.
|
|
125
|
+
*
|
|
126
|
+
* Path identity uses on-disk realpath when the worktree exists, and otherwise
|
|
127
|
+
* folds case only when the parent directory is case-insensitive. A case-sensitive
|
|
128
|
+
* Windows directory can host two worktrees that differ only by case; those
|
|
129
|
+
* must not share an admin entry.
|
|
130
|
+
*/
|
|
131
|
+
function pruneEvaluatorWorktreeAdmin(git, projectRoot, worktreePath) {
|
|
132
|
+
const common = git(["rev-parse", "--git-common-dir"], projectRoot);
|
|
133
|
+
const trimmed = common.stdout.trim();
|
|
134
|
+
if (common.returncode !== 0 || trimmed.length === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const commonDir = isAbsolute(trimmed) ? resolve(trimmed) : resolve(projectRoot, trimmed);
|
|
138
|
+
const worktreesDir = join(commonDir, "worktrees");
|
|
139
|
+
if (!existsSync(worktreesDir)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const needle = canonicalizeWorktreePath(worktreePath);
|
|
143
|
+
let names = [];
|
|
144
|
+
try {
|
|
145
|
+
names = readdirSync(worktreesDir);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
if (name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const gitdirFile = join(worktreesDir, name, "gitdir");
|
|
155
|
+
if (!existsSync(gitdirFile)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let recorded = "";
|
|
159
|
+
try {
|
|
160
|
+
recorded = readFileSync(gitdirFile, "utf8").trim();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const recordedWorktree = recorded.replace(/\\/g, "/").replace(/\/\.git$/u, "");
|
|
166
|
+
if (canonicalizeWorktreePath(recordedWorktree) === needle) {
|
|
167
|
+
containedRemove({
|
|
168
|
+
root: resolve(worktreesDir),
|
|
169
|
+
target: join(worktreesDir, name),
|
|
170
|
+
recursive: true,
|
|
171
|
+
mutation: false,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
30
177
|
function worktreeStillRegistered(git, projectRoot, worktreePath) {
|
|
31
178
|
const listed = git(["worktree", "list", "--porcelain"], projectRoot);
|
|
32
179
|
if (listed.returncode !== 0) {
|
|
33
180
|
return true;
|
|
34
181
|
}
|
|
35
|
-
const needle =
|
|
182
|
+
const needle = canonicalizeWorktreePath(worktreePath);
|
|
36
183
|
for (const line of listed.stdout.split(/\r?\n/u)) {
|
|
37
184
|
if (!line.startsWith("worktree ")) {
|
|
38
185
|
continue;
|
|
39
186
|
}
|
|
40
|
-
const listedPath = line.slice("worktree ".length)
|
|
187
|
+
const listedPath = canonicalizeWorktreePath(line.slice("worktree ".length));
|
|
41
188
|
if (listedPath === needle) {
|
|
42
189
|
return true;
|
|
43
190
|
}
|
|
@@ -53,13 +200,13 @@ export function removeEvaluatorWorktree(projectRoot, worktreePath, git = swarmGi
|
|
|
53
200
|
}
|
|
54
201
|
const firstError = proc.stderr.trim() || "<no stderr>";
|
|
55
202
|
forceDeleteWorktreeDir(worktreePath);
|
|
56
|
-
runner
|
|
203
|
+
pruneEvaluatorWorktreeAdmin(runner, projectRoot, worktreePath);
|
|
57
204
|
const retry = runner(["worktree", "remove", "--force", worktreePath], projectRoot);
|
|
58
205
|
if (retry.returncode === 0) {
|
|
59
206
|
return;
|
|
60
207
|
}
|
|
61
208
|
forceDeleteWorktreeDir(worktreePath);
|
|
62
|
-
runner
|
|
209
|
+
pruneEvaluatorWorktreeAdmin(runner, projectRoot, worktreePath);
|
|
63
210
|
if (!worktreeStillRegistered(runner, projectRoot, worktreePath) && !existsSync(worktreePath)) {
|
|
64
211
|
return;
|
|
65
212
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
export declare function projectDefinitionPath(projectRoot: string): string;
|
|
5
|
-
export declare function loadProjectDefinitionForMutation(projectRoot: string): [Record<string, unknown>, string];
|
|
6
|
-
export declare function atomicWriteProjectDefinition(path: string, data: Record<string, unknown>): void;
|
|
1
|
+
export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
|
|
2
|
+
export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
|
|
7
3
|
export declare function recordSubscriptionChange(projectRoot: string, options: {
|
|
8
4
|
op: string;
|
|
9
5
|
label?: string | null;
|
|
@@ -1,52 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFileSync,
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
6
4
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
7
|
-
import {
|
|
5
|
+
import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
|
|
8
6
|
import { resolveTriageCachePath } from "../cache-path.js";
|
|
9
7
|
import { SUBSCRIPTION_HISTORY_SCHEMA } from "./constants.js";
|
|
10
8
|
import { pyStrRepr } from "./python-repr.js";
|
|
11
9
|
import { utcIso } from "./time.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
if (!existsSync(path)) {
|
|
21
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / ` +
|
|
22
|
-
"task triage:bootstrap to scaffold one first.");
|
|
23
|
-
}
|
|
24
|
-
let raw;
|
|
25
|
-
try {
|
|
26
|
-
raw = readFileSync(path, "utf8");
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
throw new ProjectDefinitionIOError(`Could not read PROJECT-DEFINITION at ${path}: ${String(err)}`);
|
|
30
|
-
}
|
|
31
|
-
let data;
|
|
32
|
-
try {
|
|
33
|
-
data = JSON.parse(raw);
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(err)}`);
|
|
37
|
-
}
|
|
38
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
39
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
40
|
-
}
|
|
41
|
-
return [data, path];
|
|
42
|
-
}
|
|
43
|
-
export function atomicWriteProjectDefinition(path, data) {
|
|
44
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
-
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
46
|
-
const tmp = join(tmpdir(), `${randomUUID()}.tmp`);
|
|
47
|
-
writeFileSync(tmp, payload, "utf8");
|
|
48
|
-
renameSync(tmp, path);
|
|
49
|
-
}
|
|
10
|
+
// One canonical PROJECT-DEFINITION identity (#3796). The resolver, loader,
|
|
11
|
+
// error type, and write sink are the shared implementations the mutation
|
|
12
|
+
// capability binds. Module-local duplicates used to resolve the layout path
|
|
13
|
+
// directly -- ignoring the `DEFT_PROJECT_PATH` override the lock honours -- and
|
|
14
|
+
// wrote through an uncontained temp sink, so a caller could lock one artifact
|
|
15
|
+
// and load or persist another.
|
|
16
|
+
export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
|
|
17
|
+
export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
|
|
50
18
|
function resolveActor(actor) {
|
|
51
19
|
if (typeof actor === "string" && actor.trim())
|
|
52
20
|
return actor;
|
|
@@ -133,11 +101,11 @@ export function subscribe(projectRoot, options) {
|
|
|
133
101
|
// Serialise the read-modify-write + subscription-history append under the
|
|
134
102
|
// shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
|
|
135
103
|
// an update or emit out-of-order audit rows (#1260).
|
|
136
|
-
return
|
|
137
|
-
const
|
|
104
|
+
return withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
105
|
+
const data = mutation.load();
|
|
138
106
|
const plan = data.plan;
|
|
139
107
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
140
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
108
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
|
|
141
109
|
}
|
|
142
110
|
const planRec = plan;
|
|
143
111
|
migrateLegacyPolicyKey(planRec);
|
|
@@ -145,14 +113,14 @@ export function subscribe(projectRoot, options) {
|
|
|
145
113
|
planRec[PLAN_POLICY_KEY] = {};
|
|
146
114
|
const policy = planRec[PLAN_POLICY_KEY];
|
|
147
115
|
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
148
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
116
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan.policy' key`);
|
|
149
117
|
}
|
|
150
118
|
const policyRec = policy;
|
|
151
119
|
if (policyRec.triageScope === undefined)
|
|
152
120
|
policyRec.triageScope = [];
|
|
153
121
|
const rules = policyRec.triageScope;
|
|
154
122
|
if (!Array.isArray(rules)) {
|
|
155
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
123
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-list 'plan.policy.triageScope'`);
|
|
156
124
|
}
|
|
157
125
|
const before = snapshotRules(rules);
|
|
158
126
|
let changed;
|
|
@@ -168,7 +136,7 @@ export function subscribe(projectRoot, options) {
|
|
|
168
136
|
}
|
|
169
137
|
if (!changed)
|
|
170
138
|
return [false, message];
|
|
171
|
-
|
|
139
|
+
mutation.persist(data);
|
|
172
140
|
recordSubscriptionChange(projectRoot, {
|
|
173
141
|
op: "subscribe",
|
|
174
142
|
label: options.label ?? null,
|
|
@@ -185,11 +153,11 @@ export function addIgnore(projectRoot, label) {
|
|
|
185
153
|
throw new Error(`label must be a non-empty string; got ${JSON.stringify(label)}`);
|
|
186
154
|
// Serialise the read-modify-write + subscription-history append under the
|
|
187
155
|
// shared PROJECT-DEFINITION mutation lock (#1260).
|
|
188
|
-
return
|
|
189
|
-
const
|
|
156
|
+
return withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
157
|
+
const data = mutation.load();
|
|
190
158
|
const plan = data.plan;
|
|
191
159
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
192
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
160
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
|
|
193
161
|
}
|
|
194
162
|
const planRec = plan;
|
|
195
163
|
migrateLegacyPolicyKey(planRec);
|
|
@@ -197,14 +165,14 @@ export function addIgnore(projectRoot, label) {
|
|
|
197
165
|
planRec[PLAN_POLICY_KEY] = {};
|
|
198
166
|
const policy = planRec[PLAN_POLICY_KEY];
|
|
199
167
|
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
200
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
168
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan.policy' key`);
|
|
201
169
|
}
|
|
202
170
|
const policyRec = policy;
|
|
203
171
|
if (policyRec.triageScopeIgnores === undefined)
|
|
204
172
|
policyRec.triageScopeIgnores = [];
|
|
205
173
|
const raw = policyRec.triageScopeIgnores;
|
|
206
174
|
if (!Array.isArray(raw)) {
|
|
207
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
175
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-list 'plan.policy.triageScopeIgnores'`);
|
|
208
176
|
}
|
|
209
177
|
const before = snapshotRules(raw);
|
|
210
178
|
for (const entry of raw) {
|
|
@@ -216,7 +184,7 @@ export function addIgnore(projectRoot, label) {
|
|
|
216
184
|
}
|
|
217
185
|
}
|
|
218
186
|
raw.push({ label });
|
|
219
|
-
|
|
187
|
+
mutation.persist(data);
|
|
220
188
|
const after = snapshotRules(raw);
|
|
221
189
|
recordSubscriptionChange(projectRoot, {
|
|
222
190
|
op: "ignore-label",
|
|
@@ -1,39 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
4
1
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
5
|
-
import {
|
|
6
|
-
function loadForMutation(projectRoot) {
|
|
7
|
-
const path = resolveProjectDefinitionPath(resolve(projectRoot));
|
|
8
|
-
if (!existsSync(path)) {
|
|
9
|
-
throw new Error(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / task triage:bootstrap to scaffold one first.`);
|
|
10
|
-
}
|
|
11
|
-
let raw;
|
|
12
|
-
try {
|
|
13
|
-
raw = readFileSync(path, "utf8");
|
|
14
|
-
}
|
|
15
|
-
catch (err) {
|
|
16
|
-
throw new Error(`Could not read PROJECT-DEFINITION at ${path}: ${String(err)}`);
|
|
17
|
-
}
|
|
18
|
-
let data;
|
|
19
|
-
try {
|
|
20
|
-
data = JSON.parse(raw);
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
throw new Error(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(err)}`);
|
|
24
|
-
}
|
|
25
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
26
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
27
|
-
}
|
|
28
|
-
return [data, path];
|
|
29
|
-
}
|
|
30
|
-
function atomicWrite(path, data) {
|
|
31
|
-
const dir = dirname(path);
|
|
32
|
-
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
33
|
-
const tmp = join(dir, `.${Date.now()}.tmp`);
|
|
34
|
-
writeFileSync(tmp, payload, "utf8");
|
|
35
|
-
renameSync(tmp, path);
|
|
36
|
-
}
|
|
2
|
+
import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
|
|
37
3
|
/** Append a label/milestone ignore entry — mirrors Python `add_ignore`. */
|
|
38
4
|
export function addIgnore(projectRoot, options) {
|
|
39
5
|
const hasLabel = options.label !== undefined;
|
|
@@ -48,11 +14,11 @@ export function addIgnore(projectRoot, options) {
|
|
|
48
14
|
}
|
|
49
15
|
// Serialise the read-modify-write under the shared PROJECT-DEFINITION
|
|
50
16
|
// mutation lock so concurrent mutators cannot lose an update (#1260).
|
|
51
|
-
return
|
|
52
|
-
const
|
|
17
|
+
return withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
18
|
+
const data = mutation.load();
|
|
53
19
|
const plan = data.plan;
|
|
54
20
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
55
|
-
throw new Error(`PROJECT-DEFINITION at ${
|
|
21
|
+
throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
|
|
56
22
|
}
|
|
57
23
|
const planRec = plan;
|
|
58
24
|
migrateLegacyPolicyKey(planRec);
|
|
@@ -74,7 +40,7 @@ export function addIgnore(projectRoot, options) {
|
|
|
74
40
|
}
|
|
75
41
|
}
|
|
76
42
|
raw.push({ [key]: value });
|
|
77
|
-
|
|
43
|
+
mutation.persist(data);
|
|
78
44
|
return { changed: true, message: `added ignore (${key}=${value})` };
|
|
79
45
|
});
|
|
80
46
|
}
|
|
@@ -3,12 +3,9 @@ export declare const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-cha
|
|
|
3
3
|
export declare const PROJECT_DEFINITION_REL_PATH = "xbrief/PROJECT-DEFINITION.xbrief.json";
|
|
4
4
|
/** Python ``!r``-style quoting for parity with triage_subscribe.py messages. */
|
|
5
5
|
export declare function pyRepr(value: string): string;
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
}
|
|
6
|
+
export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
|
|
7
|
+
export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
|
|
9
8
|
type TriageRule = Record<string, unknown>;
|
|
10
|
-
export declare function loadProjectDefinitionForMutation(projectRoot: string): [Record<string, unknown>, string];
|
|
11
|
-
export declare function atomicWriteProjectDefinition(path: string, data: Record<string, unknown>): void;
|
|
12
9
|
export declare function recordSubscriptionChange(projectRoot: string, options: {
|
|
13
10
|
op: string;
|
|
14
11
|
label?: string | null;
|
|
@@ -34,5 +31,4 @@ export declare function unsubscribe(projectRoot: string, options?: {
|
|
|
34
31
|
actor?: string | null;
|
|
35
32
|
}): [boolean, string];
|
|
36
33
|
export declare const RECONCILE_HINT: string;
|
|
37
|
-
export {};
|
|
38
34
|
//# sourceMappingURL=index.d.ts.map
|