@try-works/dsh-recursive-mode 0.4.3 → 0.4.5
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/README.md +20 -15
- package/lib/client/index.d.ts +3 -1
- package/lib/client/settings-view.d.ts +97 -0
- package/lib/client/settings.d.ts +25 -1
- package/lib/client.js +530 -11
- package/lib/enforcement.d.ts +37 -0
- package/lib/errors.d.ts +12 -0
- package/lib/goals-projection.d.ts +34 -4
- package/lib/index.js +594 -59
- package/lib/memory-feedback.d.ts +25 -2
- package/lib/recursive_ask.tool.d.ts +60 -0
- package/lib/recursive_init.tool.d.ts +15 -0
- package/lib/run-start.d.ts +55 -0
- package/lib/runtime.d.ts +137 -31
- package/package.json +1 -1
- package/scripts/test-recursive-mode-smoke.ts +5 -1
- package/src/client/index.ts +3 -1
- package/src/client/settings-view.ts +211 -0
- package/src/client/settings.tsx +194 -8
- package/src/client/slots.ts +15 -2
- package/src/client/styles.ts +237 -0
- package/src/enforcement.ts +74 -3
- package/src/errors.ts +12 -0
- package/src/goals-projection.ts +48 -7
- package/src/index.ts +56 -10
- package/src/memory-feedback.ts +50 -5
- package/src/phase-rules.ts +10 -1
- package/src/policy-globs.ts +25 -3
- package/src/policy-write.ts +1 -1
- package/src/recursive_ask.tool.ts +218 -17
- package/src/recursive_init.tool.ts +16 -1
- package/src/run-start.ts +123 -0
- package/src/runtime.ts +186 -14
package/lib/index.js
CHANGED
|
@@ -1490,14 +1490,22 @@ function lockOrderRule(artifact, runDir) {
|
|
|
1490
1490
|
/**
|
|
1491
1491
|
* Locked-artifact write rule: a denial when the target carries
|
|
1492
1492
|
* `Status: LOCKED`, `null` otherwise. Only a run-tree `*.md` is a candidate —
|
|
1493
|
-
* the same admission test the pre-T16 branch used
|
|
1493
|
+
* the same admission test the pre-T16 branch used, now asked of the RESOLVED
|
|
1494
|
+
* path as well (see the ADMISSION note below).
|
|
1495
|
+
*
|
|
1496
|
+
* A caller with no `worktreeRoot` still gets `null` for every target, absolute
|
|
1497
|
+
* ones included: that is the pre-existing behaviour and it is left alone here —
|
|
1498
|
+
* the guard always carries a root, so nothing that reaches it changes.
|
|
1494
1499
|
*/
|
|
1495
1500
|
function lockedWriteRule(target, worktreeRoot) {
|
|
1496
1501
|
if (!target || !worktreeRoot) return null;
|
|
1497
1502
|
const normalized = target.replace(/\\/g, "/");
|
|
1498
|
-
if (!normalized.endsWith(".md")
|
|
1503
|
+
if (!normalized.endsWith(".md")) return null;
|
|
1499
1504
|
const abs = resolveFrom(worktreeRoot, normalized);
|
|
1500
|
-
if (!abs
|
|
1505
|
+
if (!abs) return null;
|
|
1506
|
+
const resolved = abs.replace(/\\/g, "/");
|
|
1507
|
+
if (!normalized.includes("/.recursive/run/") && !resolved.includes("/.recursive/run/")) return null;
|
|
1508
|
+
if (getLockStatus(abs) !== "LOCKED") return null;
|
|
1501
1509
|
return {
|
|
1502
1510
|
verdict: "deny",
|
|
1503
1511
|
detail: normalized + " carries Status: LOCKED (reopen explicitly to edit)"
|
|
@@ -2123,7 +2131,7 @@ function resolveFrom(worktreeRoot, target) {
|
|
|
2123
2131
|
const normalized = target.replace(/\\/g, "/").trim();
|
|
2124
2132
|
if (!normalized) return null;
|
|
2125
2133
|
if (/^[A-Za-z]:\//.test(normalized) || normalized.startsWith("/")) return resolve(normalized);
|
|
2126
|
-
return resolve(join(worktreeRoot, normalized.replace(
|
|
2134
|
+
return resolve(join(worktreeRoot, normalized.replace(/^\.\//, "")));
|
|
2127
2135
|
}
|
|
2128
2136
|
/** The tool-target path of a call (same key order enforcement.ts uses). */
|
|
2129
2137
|
function policyTargetPath(args) {
|
|
@@ -5454,6 +5462,18 @@ const TOOL_ERRORS = {
|
|
|
5454
5462
|
problem: "the run has unresolved delegated work, so this phase cannot lock yet",
|
|
5455
5463
|
next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again"
|
|
5456
5464
|
},
|
|
5465
|
+
RUN_START_NO_CHANNEL: {
|
|
5466
|
+
code: "RM5502",
|
|
5467
|
+
klass: "runtime",
|
|
5468
|
+
problem: "the run-start gate needs an answer, and this composition mounts no user-questions channel to ask one directly",
|
|
5469
|
+
next: "call recursive_ask with gate: run-start and no answer to surface the question, then retry with answer: \"Start run\""
|
|
5470
|
+
},
|
|
5471
|
+
RUN_START_UNANSWERED: {
|
|
5472
|
+
code: "RM5503",
|
|
5473
|
+
klass: "runtime",
|
|
5474
|
+
problem: "the user-questions channel mounted in this composition refused the run-start question, so no person was asked",
|
|
5475
|
+
next: "use recursive_ask without an answer to surface the question, and retry it with answer: \"Start run\" once the user has approved the run start"
|
|
5476
|
+
},
|
|
5457
5477
|
RUNTIME_REFUSED: {
|
|
5458
5478
|
code: "RM5501",
|
|
5459
5479
|
klass: "runtime",
|
|
@@ -5993,13 +6013,41 @@ function reviewBundleDir(root, runId) {
|
|
|
5993
6013
|
* discipline the lock receipts, the closeout receipts and the selection output all follow.
|
|
5994
6014
|
*/
|
|
5995
6015
|
/** Where the machine-owned counters live — inside the memory plane, but never a shard a person writes. */
|
|
5996
|
-
const FEEDBACK_FILE = "memory/.feedback.json";
|
|
6016
|
+
const FEEDBACK_FILE = ".recursive/memory/.feedback.json";
|
|
6017
|
+
/**
|
|
6018
|
+
* WHERE THE COUNTERS LIVED BEFORE the constant above carried its `.recursive/` prefix.
|
|
6019
|
+
*
|
|
6020
|
+
* ⚠ READ, BUT DELIBERATELY NEITHER MOVED NOR DELETED. Read, because evidence a previous run recorded is
|
|
6021
|
+
* not the plugin's to discard, and without the fallback the first settle after the move would start the
|
|
6022
|
+
* new file from an empty book — a counter lost silently, which is the one outcome ruled out. NOT moved,
|
|
6023
|
+
* because a delete is the single action that could re-create the very defect the prefix fixes: a legacy
|
|
6024
|
+
* file that is TRACKED and COMMITTED is absent from the run's diff while it is clean, so removing it
|
|
6025
|
+
* mid-run puts `D memory/.feedback.json` into the diff of every diff-audited phase authored before the
|
|
6026
|
+
* delete, which is the same retro-invalidation. Left alone, an untracked legacy file is in the diff from
|
|
6027
|
+
* the run's first phase (and is therefore accounted for), while a committed one stays invisible.
|
|
6028
|
+
* `readFeedback` prefers {@link FEEDBACK_FILE}, so the legacy counters are folded forward by the next
|
|
6029
|
+
* settle and this file then only sits there; it is machine-owned and disposable, so delete it by hand.
|
|
6030
|
+
*/
|
|
6031
|
+
const LEGACY_FEEDBACK_FILE = "memory/.feedback.json";
|
|
5997
6032
|
/** Where a run records what it was shown. */
|
|
5998
6033
|
const INJECTIONS_FILE = "memory-injections.json";
|
|
5999
|
-
/**
|
|
6034
|
+
/**
|
|
6035
|
+
* Read the counters. A missing or unreadable file is an empty book, never an error.
|
|
6036
|
+
*
|
|
6037
|
+
* ⚠ THE LEGACY PATH IS A FALLBACK AND ONLY A FALLBACK: it is consulted when — and only when — there is
|
|
6038
|
+
* no usable file at {@link FEEDBACK_FILE} yet, which is exactly the first run after the path moved. The
|
|
6039
|
+
* two are never merged, because they are two SNAPSHOTS of one counter and adding them would count a run
|
|
6040
|
+
* twice. A file that exists at the current path but does not parse stays an empty book, which is what
|
|
6041
|
+
* this function has always promised: a corrupt sidecar is not an invitation to read a different file.
|
|
6042
|
+
*/
|
|
6000
6043
|
function readFeedback(root, readFile = defaultRead$1) {
|
|
6001
|
-
const
|
|
6002
|
-
if (
|
|
6044
|
+
const current = readFile(join(root, FEEDBACK_FILE));
|
|
6045
|
+
if (current !== null && current.trim() !== "") return parseBook(current);
|
|
6046
|
+
const legacy = readFile(join(root, LEGACY_FEEDBACK_FILE));
|
|
6047
|
+
return legacy === null ? {} : parseBook(legacy);
|
|
6048
|
+
}
|
|
6049
|
+
/** One counters file as a book: anything unreadable or unshaped is `{}`, never an error. */
|
|
6050
|
+
function parseBook(text) {
|
|
6003
6051
|
try {
|
|
6004
6052
|
const parsed = JSON.parse(text);
|
|
6005
6053
|
if (typeof parsed !== "object" || parsed === null) return {};
|
|
@@ -6075,7 +6123,9 @@ function settleInjections(root, runDir, lockedPhases, write = defaultWrite, read
|
|
|
6075
6123
|
counter.applied += 1;
|
|
6076
6124
|
book[record.source] = counter;
|
|
6077
6125
|
}
|
|
6078
|
-
|
|
6126
|
+
const feedbackPath = join(root, FEEDBACK_FILE);
|
|
6127
|
+
mkdirSync(dirname(feedbackPath), { recursive: true });
|
|
6128
|
+
write(feedbackPath, JSON.stringify(sortBook(book), null, 2) + "\n");
|
|
6079
6129
|
return book;
|
|
6080
6130
|
}
|
|
6081
6131
|
/**
|
|
@@ -6781,6 +6831,123 @@ function extractAndGroup(runner, env, options = {}) {
|
|
|
6781
6831
|
};
|
|
6782
6832
|
}
|
|
6783
6833
|
//#endregion
|
|
6834
|
+
//#region src/run-start.ts
|
|
6835
|
+
/**
|
|
6836
|
+
* PHASE 0 — STARTING A RUN IS A HUMAN DECISION, NOT A SIDE EFFECT OF SCAFFOLDING.
|
|
6837
|
+
*
|
|
6838
|
+
* THE DEFECT THIS CLOSES. `recursive_init` scaffolded a run and the plugin then CREATED AND ARMED a
|
|
6839
|
+
* goal for it in the same breath (`syncRunGoal`'s "no current goal -> create and arm" branch). A goal
|
|
6840
|
+
* is not a label: `goals.create` returns an ARMED view, and the harness immediately begins driving
|
|
6841
|
+
* autonomous goal rounds for the session. So asking for a run spec was enough to start an unattended
|
|
6842
|
+
* run — the owner's rule is the opposite: *"creating a spec before a run exists should not create a
|
|
6843
|
+
* goal. Phase 0 requires explicit approval to start a run and goal."*
|
|
6844
|
+
*
|
|
6845
|
+
* WHAT "APPROVAL" IS, EXACTLY. The approving label of the `run-start` gate of `recursive_ask`
|
|
6846
|
+
* (`Start run`, as opposed to `Hold`), recorded here as a durable `- Run Start: Start run` line in the
|
|
6847
|
+
* run's Phase 0 requirements artifact. Three things make that an explicit human act rather than an
|
|
6848
|
+
* inference:
|
|
6849
|
+
*
|
|
6850
|
+
* 1. NO DEFAULT, AND THE VALUE IS THE DECISION. The line is written by the gate itself into the Phase 0
|
|
6851
|
+
* requirements document, and the gate REFUSES an answer that is not one of the labels it offered.
|
|
6852
|
+
* An unoffered answer is a transcription error wearing the shape of a decision, and a `Hold` is not
|
|
6853
|
+
* an approval in any spelling — see {@link readRunStartApproval}, which matches the approving VALUE
|
|
6854
|
+
* and nothing else, so the presence of a `Run Start` line is never on its own consent.
|
|
6855
|
+
* 2. IT IS ASKED, NOT ASSUMED. When the composition mounts `ctx.userQuestions` — the harness's own
|
|
6856
|
+
* blocking human channel, the same one plan-mode's exit uses — the question is PUT TO THE PERSON and
|
|
6857
|
+
* only their selection is recorded; a caller-supplied answer cannot stand in for it, and a channel
|
|
6858
|
+
* that cannot reach anyone ends the call without a decision (RM5503). Only a composition with no
|
|
6859
|
+
* channel at all falls back to the relayed answer, which is the contract the other three gates have.
|
|
6860
|
+
* 3. THE GOAL CANNOT BE CREATED WITHOUT IT. `syncRunGoal` refuses to create a goal for a run whose
|
|
6861
|
+
* approval record is absent, in EVERY branch that would create one — not only the "no goal yet"
|
|
6862
|
+
* branch. That is the property `tests/run-start-approval.spec.ts` asserts, because a single
|
|
6863
|
+
* unguarded branch is exactly how this defect existed in the first place.
|
|
6864
|
+
*
|
|
6865
|
+
* A SPEC MAY EXIST BEFORE A RUN EXISTS, and this module does not forbid that: the scaffold, the Phase
|
|
6866
|
+
* 0 artifacts and every later phase document are all created by `recursive_init` as before. What is
|
|
6867
|
+
* withheld is the GOAL — the object that makes the harness drive rounds. A run that is scaffolded and
|
|
6868
|
+
* never approved is a spec: readable, editable, lockable, and inert.
|
|
6869
|
+
*/
|
|
6870
|
+
/** The gate id `recursive_ask` answers for a run start. Deliberately NOT in ASK_GATE_IDS. */
|
|
6871
|
+
const RUN_START_GATE_ID = "run-start";
|
|
6872
|
+
/** The Phase 0 artifact the approval is recorded in. */
|
|
6873
|
+
const RUN_START_ARTIFACT = "00-requirements.md";
|
|
6874
|
+
/** The artifact field the approval reads back from. */
|
|
6875
|
+
const RUN_START_MARKER = "Run Start";
|
|
6876
|
+
/** The approving label. The ONLY label that starts a run. */
|
|
6877
|
+
const RUN_START_APPROVE = "Start run";
|
|
6878
|
+
/**
|
|
6879
|
+
* WHY THIS GATE IS NOT IN `ASK_GATE_IDS`. Those three are the WORKFLOW's gates — phase-3 test
|
|
6880
|
+
* evidence, phase-5 sign-off, resolving a gate block — and their membership is asserted as exactly
|
|
6881
|
+
* three. Starting a run is a different kind of decision: it is the one that decides whether there is
|
|
6882
|
+
* a run at all. It lives here, with its own contract, so widening the workflow's gate list cannot
|
|
6883
|
+
* quietly widen what may start a run.
|
|
6884
|
+
*/
|
|
6885
|
+
const RUN_START_GATE = {
|
|
6886
|
+
id: RUN_START_GATE_ID,
|
|
6887
|
+
header: "Start run",
|
|
6888
|
+
question: "Approve phase 0 and start this run? Approving creates an armed goal the harness will keep driving.",
|
|
6889
|
+
options: [{
|
|
6890
|
+
label: RUN_START_APPROVE,
|
|
6891
|
+
description: "Record the approval and arm the run goal."
|
|
6892
|
+
}, {
|
|
6893
|
+
label: "Hold",
|
|
6894
|
+
description: "Leave the spec inert: no run goal, no autonomous rounds."
|
|
6895
|
+
}],
|
|
6896
|
+
marker: RUN_START_MARKER
|
|
6897
|
+
};
|
|
6898
|
+
/** Is a `run-start` answer the approving one? */
|
|
6899
|
+
function isRunStartApproval(answer) {
|
|
6900
|
+
return answer.trim() === RUN_START_APPROVE;
|
|
6901
|
+
}
|
|
6902
|
+
/** Where the Phase 0 requirements artifact lives for a run rooted at `root`. */
|
|
6903
|
+
function runStartArtifactPath(root, runId) {
|
|
6904
|
+
return join(root, ".recursive", "run", runId, RUN_START_ARTIFACT);
|
|
6905
|
+
}
|
|
6906
|
+
/** The artifact text, or null when the file is absent (a read failure is not an approval). */
|
|
6907
|
+
function readRunStartArtifact(root, runId) {
|
|
6908
|
+
try {
|
|
6909
|
+
return readFileSync(runStartArtifactPath(root, runId), "utf8");
|
|
6910
|
+
} catch {
|
|
6911
|
+
return null;
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
/**
|
|
6915
|
+
* The approval state of a run, read from its Phase 0 artifact.
|
|
6916
|
+
*
|
|
6917
|
+
* ⚠ MATCHED ON THE VALUE, NOT ON THE LINE'S PRESENCE. `getMdFieldValue` returns the field's VALUE, so
|
|
6918
|
+
* a recorded `- Run Start: Hold` is refused here — a check for "is there a Run Start line?" would read
|
|
6919
|
+
* a refusal as consent, which is the one mistake this whole module exists to prevent.
|
|
6920
|
+
*/
|
|
6921
|
+
function readRunStartApproval(root, runId) {
|
|
6922
|
+
const content = readRunStartArtifact(root, runId);
|
|
6923
|
+
if (content === null) return {
|
|
6924
|
+
approved: false,
|
|
6925
|
+
artifact: RUN_START_ARTIFACT,
|
|
6926
|
+
reason: "the Phase 0 requirements artifact does not exist yet"
|
|
6927
|
+
};
|
|
6928
|
+
const value = getMdFieldValue(content, RUN_START_MARKER);
|
|
6929
|
+
if (value === null) return {
|
|
6930
|
+
approved: false,
|
|
6931
|
+
artifact: RUN_START_ARTIFACT,
|
|
6932
|
+
reason: "no Run Start decision has been recorded"
|
|
6933
|
+
};
|
|
6934
|
+
if (!isRunStartApproval(value)) return {
|
|
6935
|
+
approved: false,
|
|
6936
|
+
artifact: RUN_START_ARTIFACT,
|
|
6937
|
+
reason: "Run Start is " + JSON.stringify(value) + ", which does not start a run"
|
|
6938
|
+
};
|
|
6939
|
+
return {
|
|
6940
|
+
approved: true,
|
|
6941
|
+
artifact: RUN_START_ARTIFACT,
|
|
6942
|
+
reason: ""
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6945
|
+
/**
|
|
6946
|
+
* The ONE refusal reason the projection returns before approval, exported so every caller branches on
|
|
6947
|
+
* the same string instead of re-typing it (a re-typed reason is a caller that silently stops matching).
|
|
6948
|
+
*/
|
|
6949
|
+
const RUN_START_NOT_APPROVED = "run not started: phase 0 approval has not been granted";
|
|
6950
|
+
//#endregion
|
|
6784
6951
|
//#region src/recursive_ask.tool.ts
|
|
6785
6952
|
/**
|
|
6786
6953
|
* T23 — `recursive_ask`: the three human gates as STRUCTURED decisions, not prose.
|
|
@@ -6808,6 +6975,14 @@ const ASK_GATE_IDS = [
|
|
|
6808
6975
|
"qa-signoff",
|
|
6809
6976
|
"gate-block"
|
|
6810
6977
|
];
|
|
6978
|
+
/** Is this gate id the run-start gate? */
|
|
6979
|
+
function isRunStartGate(gateId) {
|
|
6980
|
+
return gateId === RUN_START_GATE_ID;
|
|
6981
|
+
}
|
|
6982
|
+
/** Every gate id `recursive_ask` accepts, workflow gates first. */
|
|
6983
|
+
function askGateIds() {
|
|
6984
|
+
return [...ASK_GATE_IDS, RUN_START_GATE_ID];
|
|
6985
|
+
}
|
|
6811
6986
|
const ASK_GATES = {
|
|
6812
6987
|
"tdd-mode": {
|
|
6813
6988
|
id: "tdd-mode",
|
|
@@ -6901,6 +7076,36 @@ function buildAskQuestion(gateId) {
|
|
|
6901
7076
|
});
|
|
6902
7077
|
}
|
|
6903
7078
|
/**
|
|
7079
|
+
* PHASE 0 — build the question for ANY accepted gate, including the run-start gate.
|
|
7080
|
+
*
|
|
7081
|
+
* A separate entry point rather than a widened `buildAskQuestion` so the three workflow gates keep the
|
|
7082
|
+
* exact signature and behaviour their callers (and `runtime.phaseRules`) already rely on.
|
|
7083
|
+
*/
|
|
7084
|
+
function buildAskQuestionFor(gateId) {
|
|
7085
|
+
if (isRunStartGate(gateId)) return validateAskQuestion({
|
|
7086
|
+
id: RUN_START_GATE.id,
|
|
7087
|
+
header: RUN_START_GATE.header,
|
|
7088
|
+
question: RUN_START_GATE.question,
|
|
7089
|
+
options: RUN_START_GATE.options.map((option) => ({ ...option }))
|
|
7090
|
+
});
|
|
7091
|
+
return buildAskQuestion(gateId);
|
|
7092
|
+
}
|
|
7093
|
+
/**
|
|
7094
|
+
* PHASE 0 — validate an answer to ANY accepted gate.
|
|
7095
|
+
*
|
|
7096
|
+
* The run-start gate accepts only the labels IT offered, exactly like the other three, and the check is
|
|
7097
|
+
* the same `ASK_GATES`-shaped test against its own options. An answer of `maybe` is refused rather than
|
|
7098
|
+
* recorded, because a recorded non-answer is the failure mode this whole change exists to prevent.
|
|
7099
|
+
*/
|
|
7100
|
+
function validateAskAnswerFor(gateId, answer) {
|
|
7101
|
+
if (isRunStartGate(gateId)) {
|
|
7102
|
+
const offered = RUN_START_GATE.options.map((option) => option.label);
|
|
7103
|
+
if (!offered.includes(answer)) throw new AskValidationError("answer", "must be one of " + offered.join(" | ") + " (got " + JSON.stringify(answer) + ")");
|
|
7104
|
+
return answer;
|
|
7105
|
+
}
|
|
7106
|
+
return validateAskAnswer(gateId, answer);
|
|
7107
|
+
}
|
|
7108
|
+
/**
|
|
6904
7109
|
* Validate an answer against its gate.
|
|
6905
7110
|
*
|
|
6906
7111
|
* An answer that is not one of the offered labels is REFUSED rather than recorded: a marker saying
|
|
@@ -6949,15 +7154,19 @@ function pendingGateFor(artifactFile, artifactText) {
|
|
|
6949
7154
|
* ⚠ THE WRITE-BACK IS A MARKER LINE, REPLACED IN PLACE when the artifact already carries one. A
|
|
6950
7155
|
* second `TDD Mode:` line would leave two answers to one question and make "what was decided?"
|
|
6951
7156
|
* depend on which a reader found first.
|
|
7157
|
+
*
|
|
7158
|
+
* ⚠ PHASE 0 — `run-start` IS RECORDED BY THE PLUGIN, NEVER BY A BARE MARKER WRITE. See
|
|
7159
|
+
* `recordRunStartAnswer`: it is the only path that can arm a run goal, it prefers the blocking human
|
|
7160
|
+
* channel, and it fails closed when no person can be reached.
|
|
6952
7161
|
*/
|
|
6953
7162
|
function createRecursiveAskTool(recursive) {
|
|
6954
7163
|
return defineTool({
|
|
6955
7164
|
name: "recursive_ask",
|
|
6956
|
-
description: "Ask
|
|
7165
|
+
description: "Ask a human gate as a structured decision (tdd-mode, qa-signoff, gate-block), or ASK TO START A RUN (run-start: nothing runs, and no goal exists, until this gate is approved). Call without `answer` to ask; call with it to write the answer into the artifact as a durable marker. One ask per step.",
|
|
6957
7166
|
parameters: {
|
|
6958
7167
|
gate: {
|
|
6959
7168
|
type: "string",
|
|
6960
|
-
description: "tdd-mode | qa-signoff | gate-block. Required."
|
|
7169
|
+
description: "tdd-mode | qa-signoff | gate-block | run-start. Required. `run-start` is phase 0: approving it records the approval and arms the run goal, which is what makes the harness drive rounds."
|
|
6961
7170
|
},
|
|
6962
7171
|
runId: {
|
|
6963
7172
|
type: "string",
|
|
@@ -6965,11 +7174,11 @@ function createRecursiveAskTool(recursive) {
|
|
|
6965
7174
|
},
|
|
6966
7175
|
artifact: {
|
|
6967
7176
|
type: "string",
|
|
6968
|
-
description: "Artifact file the answer belongs in. Optional; defaults per gate (gate-block has none, so it is required for that gate)."
|
|
7177
|
+
description: "Artifact file the answer belongs in. Optional; defaults per gate (gate-block has none, so it is required for that gate; run-start is always recorded in 00-requirements.md)."
|
|
6969
7178
|
},
|
|
6970
7179
|
answer: {
|
|
6971
7180
|
type: "string",
|
|
6972
|
-
description: "One of the gate's option labels. Omit to ASK."
|
|
7181
|
+
description: "One of the gate's option labels. Omit to ASK. For run-start, the labels are: " + RUN_START_GATE.options.map((option) => option.label).join(" | ") + "."
|
|
6973
7182
|
}
|
|
6974
7183
|
},
|
|
6975
7184
|
output: {
|
|
@@ -6983,29 +7192,32 @@ function createRecursiveAskTool(recursive) {
|
|
|
6983
7192
|
const gateId = (args.gate ?? "").trim();
|
|
6984
7193
|
const runId = args.runId?.trim() ?? "";
|
|
6985
7194
|
if (runId === "") return { error: toolError("MISSING_RUN_ID") };
|
|
6986
|
-
if (!
|
|
7195
|
+
if (!askGateIds().includes(gateId)) return { error: toolError("BAD_ASK_GATE", "gate must be one of " + askGateIds().join(" | ")) };
|
|
6987
7196
|
const root = await recursive.resolveWorkspaceRoot(exec.agent);
|
|
6988
7197
|
if (!root) return { error: toolError("NO_WORKSPACE") };
|
|
6989
|
-
const artifact = (args.artifact ?? GATE_DEFAULT_ARTIFACT[gateId]).trim();
|
|
7198
|
+
const artifact = (isRunStartGate(gateId) ? RUN_START_ARTIFACT : args.artifact ?? GATE_DEFAULT_ARTIFACT[gateId]).trim();
|
|
6990
7199
|
let question;
|
|
6991
7200
|
try {
|
|
6992
|
-
question =
|
|
7201
|
+
question = buildAskQuestionFor(gateId);
|
|
6993
7202
|
} catch (err) {
|
|
6994
7203
|
return { error: toolError("BAD_ASK_GATE", err instanceof Error ? err.message : String(err)) };
|
|
6995
7204
|
}
|
|
6996
|
-
|
|
7205
|
+
const channelMounted = recursive.userQuestionsChannel !== null;
|
|
7206
|
+
if (args.answer === void 0 && !(isRunStartGate(gateId) && channelMounted)) return {
|
|
6997
7207
|
gate: gateId,
|
|
6998
|
-
marker: ASK_GATES[gateId].marker,
|
|
7208
|
+
marker: isRunStartGate(gateId) ? RUN_START_GATE.marker : ASK_GATES[gateId].marker,
|
|
6999
7209
|
artifact,
|
|
7000
7210
|
question
|
|
7001
7211
|
};
|
|
7002
7212
|
let answer;
|
|
7003
|
-
|
|
7004
|
-
|
|
7213
|
+
if (args.answer === void 0) answer = void 0;
|
|
7214
|
+
else try {
|
|
7215
|
+
answer = validateAskAnswerFor(gateId, args.answer);
|
|
7005
7216
|
} catch (err) {
|
|
7006
7217
|
return { error: toolError("BAD_ASK_ANSWER", err instanceof Error ? err.message : String(err)) };
|
|
7007
7218
|
}
|
|
7008
7219
|
if (artifact === "") return { error: toolError("MISSING_ASK_ARTIFACT", "this gate needs an explicit artifact to record into") };
|
|
7220
|
+
if (isRunStartGate(gateId)) return recordRunStartAnswer(recursive, root, runId, answer, exec);
|
|
7009
7221
|
const marker = answerMarker(gateId, answer);
|
|
7010
7222
|
const written = recursive.recordAskAnswer(root, runId, artifact, marker);
|
|
7011
7223
|
return {
|
|
@@ -7019,6 +7231,76 @@ function createRecursiveAskTool(recursive) {
|
|
|
7019
7231
|
}
|
|
7020
7232
|
});
|
|
7021
7233
|
}
|
|
7234
|
+
/**
|
|
7235
|
+
* PHASE 0 — record the answer to the run-start gate, and start the run only if it says so.
|
|
7236
|
+
*
|
|
7237
|
+
* ⚠ THIS GATE NEVER ACCEPTS A RELAYED ANSWER WHILE A HUMAN CHANNEL IS MOUNTED. That is the rule that makes
|
|
7238
|
+
* an approval a human act rather than an inference: when `ctx.userQuestions` is present, the question is
|
|
7239
|
+
* PUT TO THE PERSON and nothing else can settle it — not the caller's own `answer` argument, and not a
|
|
7240
|
+
* fabrication, because `ask()` resolves only with a real selection. A person's decline is likewise final
|
|
7241
|
+
* for that call and cannot be overridden by a model that asked for `Start run` in the same breath.
|
|
7242
|
+
*
|
|
7243
|
+
* ⚠ AND WHEN NO CHANNEL IS MOUNTED, THE RELAYED ANSWER IS THE ONLY POSSIBLE SOURCE, so it is used — that
|
|
7244
|
+
* is the same contract the other three gates have always had, and refusing it would leave a composition
|
|
7245
|
+
* without the channel unable to start any run at all. The question is surfaced first by the ASK branch
|
|
7246
|
+
* (the card data the host renders), and the model's `answer` is that person's selection coming back.
|
|
7247
|
+
*
|
|
7248
|
+
* ⚠ WHAT THE GATE THEREFORE DOES *NOT* CLAIM, stated rather than implied: in a composition with no
|
|
7249
|
+
* `userQuestions` channel, a plugin cannot verify that a person was really asked, so a model could in
|
|
7250
|
+
* principle relay a label nobody gave. That is a property of the relay, not of this gate — and it is the
|
|
7251
|
+
* reason the channel is consulted in preference whenever it exists. See the header of `run-start.ts`.
|
|
7252
|
+
*/
|
|
7253
|
+
async function recordRunStartAnswer(recursive, root, runId, answer, exec) {
|
|
7254
|
+
const channel = recursive.userQuestionsChannel;
|
|
7255
|
+
const fromChannel = channel ? await askRunStartDirectly(channel, exec) : null;
|
|
7256
|
+
if (channel !== null && fromChannel === null) return { error: toolError("RUN_START_UNANSWERED") };
|
|
7257
|
+
const final = fromChannel ?? answer;
|
|
7258
|
+
if (final === void 0) return { error: toolError("RUN_START_NO_CHANNEL") };
|
|
7259
|
+
let decided;
|
|
7260
|
+
try {
|
|
7261
|
+
decided = validateAskAnswerFor(RUN_START_GATE_ID, final);
|
|
7262
|
+
} catch (err) {
|
|
7263
|
+
return { error: toolError("BAD_ASK_ANSWER", err instanceof Error ? err.message : String(err)) };
|
|
7264
|
+
}
|
|
7265
|
+
const outcome = recursive.approveRunStart(root, runId, exec.agent, decided);
|
|
7266
|
+
return {
|
|
7267
|
+
gate: RUN_START_GATE_ID,
|
|
7268
|
+
answer: decided,
|
|
7269
|
+
artifact: RUN_START_ARTIFACT,
|
|
7270
|
+
source: fromChannel === null ? "relayed" : "user-questions",
|
|
7271
|
+
path: outcome.path,
|
|
7272
|
+
replaced: outcome.replaced,
|
|
7273
|
+
armed: outcome.ok && outcome.goal.ok,
|
|
7274
|
+
goal: outcome.goal
|
|
7275
|
+
};
|
|
7276
|
+
}
|
|
7277
|
+
/**
|
|
7278
|
+
* Ask the run-start question through the blocking channel and return the selection, or null when the
|
|
7279
|
+
* person was not reachable (no answerer, no live root agent, a dismissal, an abort).
|
|
7280
|
+
*
|
|
7281
|
+
* The selection is filtered to the gate's OWN labels before it is returned: a question a UI answered with
|
|
7282
|
+
* a free-text custom value must not become an approval just because it arrived on the right channel.
|
|
7283
|
+
*/
|
|
7284
|
+
async function askRunStartDirectly(channel, exec) {
|
|
7285
|
+
const known = RUN_START_GATE.options.map((option) => option.label);
|
|
7286
|
+
try {
|
|
7287
|
+
const selected = (await channel.ask({
|
|
7288
|
+
questions: [{
|
|
7289
|
+
id: RUN_START_GATE.id,
|
|
7290
|
+
header: RUN_START_GATE.header,
|
|
7291
|
+
question: RUN_START_GATE.question,
|
|
7292
|
+
options: RUN_START_GATE.options.map((option) => ({ ...option }))
|
|
7293
|
+
}],
|
|
7294
|
+
agent: exec.agent,
|
|
7295
|
+
signal: exec.signal,
|
|
7296
|
+
wait: { callId: exec.callId }
|
|
7297
|
+
})).answers.find((entry) => entry.id === RUN_START_GATE.id)?.selected?.filter((label) => known.includes(label)) ?? [];
|
|
7298
|
+
if (selected.length !== 1) return null;
|
|
7299
|
+
return selected[0];
|
|
7300
|
+
} catch {
|
|
7301
|
+
return null;
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7022
7304
|
//#endregion
|
|
7023
7305
|
//#region src/lifecycle.ts
|
|
7024
7306
|
/**
|
|
@@ -7234,7 +7516,20 @@ function currentPhaseArtifact(worktreeRoot, runId) {
|
|
|
7234
7516
|
best = name;
|
|
7235
7517
|
}
|
|
7236
7518
|
}
|
|
7237
|
-
|
|
7519
|
+
let inForce = "";
|
|
7520
|
+
let inForcePhase = Number.POSITIVE_INFINITY;
|
|
7521
|
+
for (const name of names) {
|
|
7522
|
+
if (!name.endsWith(".md")) continue;
|
|
7523
|
+
const phase = phaseNumberForArtifact(name);
|
|
7524
|
+
if (!phase) continue;
|
|
7525
|
+
const value = Number(phase);
|
|
7526
|
+
if (getLockStatus(join(runDir, name)) === "LOCKED") continue;
|
|
7527
|
+
if (value < inForcePhase) {
|
|
7528
|
+
inForcePhase = value;
|
|
7529
|
+
inForce = name;
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
return inForce !== "" ? inForce : best;
|
|
7238
7533
|
}
|
|
7239
7534
|
function evaluateToolGuard(exec, worktreeRoot, activeRunId, mode = "advisory") {
|
|
7240
7535
|
const name = exec.name;
|
|
@@ -7359,14 +7654,58 @@ function resolveTargetPath(target, worktreeRoot) {
|
|
|
7359
7654
|
return abs;
|
|
7360
7655
|
}
|
|
7361
7656
|
/**
|
|
7657
|
+
* The ADMISSION test for the tamper path: the resolved absolute path when
|
|
7658
|
+
* `targetPath` names a run-tree `*.md` — a tamper CANDIDATE — and `null`
|
|
7659
|
+
* otherwise. Pure path arithmetic on every branch (no filesystem work), so a
|
|
7660
|
+
* caller may use it as a cheap shape check before paying for `existsSync`.
|
|
7661
|
+
*
|
|
7662
|
+
* ⚠ THE BLIND SPOT THIS FUNCTION EXISTS TO CLOSE. The admission test used to be a
|
|
7663
|
+
* substring test on the target STRING alone, looking for `/.recursive/run/`. A
|
|
7664
|
+
* repo-relative path has no separator before `.recursive`, so
|
|
7665
|
+
* `.recursive/run/<id>/00-requirements.md` — the spelling a model actually types,
|
|
7666
|
+
* and its backslash form — was rejected before ANYTHING was examined, and
|
|
7667
|
+
* tampering with a locked artifact through that spelling was invisible
|
|
7668
|
+
* (measured: `detectTamper` returned a record for the absolute path and `null`
|
|
7669
|
+
* for the relative one, on the same file). The identical defect, in the identical
|
|
7670
|
+
* spelling, was fixed one module over in `policy-globs.ts` `lockedWriteRule`; this
|
|
7671
|
+
* is that fix's shape, reused rather than reinvented.
|
|
7672
|
+
*
|
|
7673
|
+
* So the marker is looked for on the path the target RESOLVES to as well as on
|
|
7674
|
+
* the string as written. The `||` is load-bearing and the string test is KEPT
|
|
7675
|
+
* rather than replaced, because a resolved-only test would SHRINK the admitted
|
|
7676
|
+
* set: an absolute target that literally carries the marker but resolves away
|
|
7677
|
+
* from it (`…/.recursive/run/../…`) was caught before and must stay caught. The
|
|
7678
|
+
* net effect is a strict SUPERSET of the previous behaviour, so no tamper that
|
|
7679
|
+
* was visible before can become invisible.
|
|
7680
|
+
*
|
|
7681
|
+
* EXPORTED because `src/index.ts`'s `fs/observed` listener must apply the SAME
|
|
7682
|
+
* admission test before calling `detectTamper`. That listener used to carry a
|
|
7683
|
+
* hand-copied mirror of this test, and a mirror is exactly what leaves half the
|
|
7684
|
+
* defect behind: widening `detectTamper` alone changes nothing, because the
|
|
7685
|
+
* listener rejects the spelling first. One function cannot disagree with itself.
|
|
7686
|
+
*/
|
|
7687
|
+
function tamperCandidatePath(targetPath, worktreeRoot) {
|
|
7688
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
7689
|
+
if (!normalized.endsWith(".md")) return null;
|
|
7690
|
+
const abs = resolveTargetPath(normalized, worktreeRoot);
|
|
7691
|
+
if (!abs) return null;
|
|
7692
|
+
const resolved = abs.replace(/\\/g, "/");
|
|
7693
|
+
if (!normalized.includes("/.recursive/run/") && !resolved.includes("/.recursive/run/")) return null;
|
|
7694
|
+
return abs;
|
|
7695
|
+
}
|
|
7696
|
+
/**
|
|
7362
7697
|
* Layer 8 - fs/observed lock-tamper detection.
|
|
7363
7698
|
* A locked *.md whose observed version differs from the stored LockHash is
|
|
7364
7699
|
* a tamper. Returns a tamper reason (or null when clean/not-applicable).
|
|
7700
|
+
*
|
|
7701
|
+
* The admission test lives in `tamperCandidatePath` (above), shared with the
|
|
7702
|
+
* `fs/observed` listener in `src/index.ts` — see the note there for why sharing
|
|
7703
|
+
* it is the point and not a tidiness preference. What this function reports is
|
|
7704
|
+
* unchanged: the same record shape, carrying the target AS WRITTEN.
|
|
7365
7705
|
*/
|
|
7366
7706
|
function detectTamper(targetPath, worktreeRoot, activeRunId) {
|
|
7367
7707
|
const normalized = targetPath.replace(/\\/g, "/");
|
|
7368
|
-
|
|
7369
|
-
const abs = resolveTargetPath(normalized, worktreeRoot);
|
|
7708
|
+
const abs = tamperCandidatePath(normalized, worktreeRoot);
|
|
7370
7709
|
if (!abs || !existsSync(abs)) return null;
|
|
7371
7710
|
if (getLockStatus(abs) === "STALE_LOCK") return {
|
|
7372
7711
|
runId: activeRunId,
|
|
@@ -8816,8 +9155,25 @@ function mutatePhase(service, agent, ref, target) {
|
|
|
8816
9155
|
* Sync a run's durable goal to the requested phase. Safe: never touches a goal
|
|
8817
9156
|
* whose objective is not this run's marker, and never re-creates over a
|
|
8818
9157
|
* non-complete foreign goal.
|
|
9158
|
+
*
|
|
9159
|
+
* ⚠ `approved` IS THE PHASE-0 GATE, and it defaults to the SAFE direction. A goal is not a label:
|
|
9160
|
+
* `create` returns an ARMED view and the harness starts driving autonomous goal rounds for the
|
|
9161
|
+
* session, so creating one is starting the run. The owner's rule is that phase 0 requires explicit
|
|
9162
|
+
* approval, which means the projection must be unable to arm anything on its own — hence a default of
|
|
9163
|
+
* `false` and an explicit refusal in EVERY branch that would call `create`, including the two
|
|
9164
|
+
* replace-a-completed-goal branches (an unapproved run cannot have reached `complete`, but "cannot
|
|
9165
|
+
* happen" is what the single unguarded branch relied on too).
|
|
9166
|
+
*
|
|
9167
|
+
* ⚠ AND IT IS REACHED ON ORDINARY WORK, so the unapproved path is QUIET AND IDEMPOTENT: no goal is
|
|
9168
|
+
* created, nothing is written, no error is thrown, and the run's artifacts are untouched. The caller
|
|
9169
|
+
* reads {@link RUN_START_NOT_APPROVED} to tell "this run has not been started yet" apart from a real
|
|
9170
|
+
* failure, so a normal phase step never surfaces a warning.
|
|
9171
|
+
*
|
|
9172
|
+
* `approved` is passed IN rather than read here because this module is pure: it takes the goal service
|
|
9173
|
+
* seam and nothing else, and the plugin's own filesystem reads live in the runtime (see
|
|
9174
|
+
* `RecursiveRuntime.readRunStartApproval`).
|
|
8819
9175
|
*/
|
|
8820
|
-
function syncRunGoal(service, agent, runId, runState) {
|
|
9176
|
+
function syncRunGoal(service, agent, runId, runState, approved = false) {
|
|
8821
9177
|
if (!service) return {
|
|
8822
9178
|
ok: false,
|
|
8823
9179
|
reason: "no goals service"
|
|
@@ -8832,12 +9188,18 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8832
9188
|
phase: target,
|
|
8833
9189
|
ref
|
|
8834
9190
|
};
|
|
8835
|
-
if (phase === "complete")
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
9191
|
+
if (phase === "complete") {
|
|
9192
|
+
if (!approved) return {
|
|
9193
|
+
ok: false,
|
|
9194
|
+
reason: RUN_START_NOT_APPROVED
|
|
9195
|
+
};
|
|
9196
|
+
return {
|
|
9197
|
+
ok: true,
|
|
9198
|
+
phase: target,
|
|
9199
|
+
ref: refOf(service.create(agent, { objective: goalObjective(runId, runState) })),
|
|
9200
|
+
created: true
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
8841
9203
|
return mutatePhase(service, agent, ref, target) ? {
|
|
8842
9204
|
ok: true,
|
|
8843
9205
|
phase: target,
|
|
@@ -8848,17 +9210,27 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8848
9210
|
};
|
|
8849
9211
|
}
|
|
8850
9212
|
if (current) {
|
|
8851
|
-
if (current.phase === "complete")
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
9213
|
+
if (current.phase === "complete") {
|
|
9214
|
+
if (!approved) return {
|
|
9215
|
+
ok: false,
|
|
9216
|
+
reason: RUN_START_NOT_APPROVED
|
|
9217
|
+
};
|
|
9218
|
+
return {
|
|
9219
|
+
ok: true,
|
|
9220
|
+
phase: target,
|
|
9221
|
+
ref: refOf(service.create(agent, { objective: goalObjective(runId, runState) })),
|
|
9222
|
+
created: true
|
|
9223
|
+
};
|
|
9224
|
+
}
|
|
8857
9225
|
return {
|
|
8858
9226
|
ok: false,
|
|
8859
9227
|
reason: "a non-matching active goal exists (foreign goal not touched)"
|
|
8860
9228
|
};
|
|
8861
9229
|
}
|
|
9230
|
+
if (!approved) return {
|
|
9231
|
+
ok: false,
|
|
9232
|
+
reason: RUN_START_NOT_APPROVED
|
|
9233
|
+
};
|
|
8862
9234
|
return {
|
|
8863
9235
|
ok: true,
|
|
8864
9236
|
phase: target,
|
|
@@ -8866,7 +9238,13 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8866
9238
|
created: true
|
|
8867
9239
|
};
|
|
8868
9240
|
}
|
|
8869
|
-
/**
|
|
9241
|
+
/**
|
|
9242
|
+
* Block the current run goal (used on a gate-block). Never touches a foreign goal.
|
|
9243
|
+
*
|
|
9244
|
+
* ⚠ A RUN THAT WAS NEVER STARTED HAS NO GOAL TO BLOCK, so this reports the unapproved state in the
|
|
9245
|
+
* same words as {@link syncRunGoal} rather than "no current goal to block": the caller's question is
|
|
9246
|
+
* "why is there no goal", and the answer must not depend on which entry point happened to ask.
|
|
9247
|
+
*/
|
|
8870
9248
|
function blockRunGoal(service, agent, runId, reason) {
|
|
8871
9249
|
if (!service) return {
|
|
8872
9250
|
ok: false,
|
|
@@ -8875,7 +9253,7 @@ function blockRunGoal(service, agent, runId, reason) {
|
|
|
8875
9253
|
const current = service.get(agent);
|
|
8876
9254
|
if (!current) return {
|
|
8877
9255
|
ok: false,
|
|
8878
|
-
reason:
|
|
9256
|
+
reason: RUN_START_NOT_APPROVED
|
|
8879
9257
|
};
|
|
8880
9258
|
if (!isRunGoal(current, runId)) return {
|
|
8881
9259
|
ok: false,
|
|
@@ -8891,9 +9269,16 @@ function blockRunGoal(service, agent, runId, reason) {
|
|
|
8891
9269
|
reason: "goal block failed"
|
|
8892
9270
|
};
|
|
8893
9271
|
}
|
|
8894
|
-
/**
|
|
8895
|
-
|
|
8896
|
-
|
|
9272
|
+
/**
|
|
9273
|
+
* Bridge a run's blocked goal back to active (used on a reopen).
|
|
9274
|
+
*
|
|
9275
|
+
* ⚠ REOPEN IS NOT A BACK DOOR TO STARTING A RUN. It routes through {@link syncRunGoal}, so a reopen of
|
|
9276
|
+
* an unapproved run cannot create the goal that init deliberately withheld. An APPROVED run is
|
|
9277
|
+
* unaffected: its approval outlives the reopen, because the approval is a durable line in the run's
|
|
9278
|
+
* own Phase 0 artifact rather than a value held in memory (verified in `tests/run-start-approval.spec.ts`).
|
|
9279
|
+
*/
|
|
9280
|
+
function resumeRunGoal(service, agent, runId, approved = false) {
|
|
9281
|
+
return syncRunGoal(service, agent, runId, "active", approved);
|
|
8897
9282
|
}
|
|
8898
9283
|
//#endregion
|
|
8899
9284
|
//#region src/teams-loop.ts
|
|
@@ -9503,9 +9888,23 @@ var RecursiveRuntime = class extends Service {
|
|
|
9503
9888
|
this.subagentsSeam = config.subagents ?? null;
|
|
9504
9889
|
this.workflow = config.workflow ?? null;
|
|
9505
9890
|
}
|
|
9506
|
-
/**
|
|
9891
|
+
/**
|
|
9892
|
+
* T10: the native jobs registry, when the composition mounts one. */
|
|
9507
9893
|
jobs;
|
|
9508
9894
|
/**
|
|
9895
|
+
* PHASE 0 — attach the goals service after construction.
|
|
9896
|
+
*
|
|
9897
|
+
* The composition resolves `goals` with ONE `ctx.get` at apply time and passes it to the constructor,
|
|
9898
|
+
* which is fine for a service that is already mounted. This seam exists for the two cases that pattern
|
|
9899
|
+
* cannot cover: a composition that mounts `goals` later (the same late-attach reason `attachSubagents`
|
|
9900
|
+
* and `attachLlmInventory` exist), and a test that needs the REAL runtime wired to a structural fake —
|
|
9901
|
+
* a fake passed through the plugin's Config is dropped, because the Config schema is the settings
|
|
9902
|
+
* namespace and strips keys it does not declare.
|
|
9903
|
+
*/
|
|
9904
|
+
attachGoals(service) {
|
|
9905
|
+
this.goalsService = service;
|
|
9906
|
+
}
|
|
9907
|
+
/**
|
|
9509
9908
|
* T23 — write a gate's answer into an artifact as a marker line.
|
|
9510
9909
|
*
|
|
9511
9910
|
* ⚠ REPLACED IN PLACE when the artifact already carries that gate's marker: two `TDD Mode:` lines
|
|
@@ -9697,16 +10096,72 @@ var RecursiveRuntime = class extends Service {
|
|
|
9697
10096
|
return report;
|
|
9698
10097
|
}
|
|
9699
10098
|
/**
|
|
10099
|
+
* PHASE 0 — read a run's start approval from its own Phase 0 artifact.
|
|
10100
|
+
*
|
|
10101
|
+
* The approval is a DURABLE line in `.recursive/run/<runId>/00-requirements.md`, not a value held in
|
|
10102
|
+
* memory, for the reason every other gate here is durable: a decision that only exists in a session
|
|
10103
|
+
* cannot be cited, and cannot survive the session it was made in. Read-only; asking changes nothing.
|
|
10104
|
+
*/
|
|
10105
|
+
readRunStartApproval(root, runId) {
|
|
10106
|
+
return readRunStartApproval(root, runId);
|
|
10107
|
+
}
|
|
10108
|
+
/**
|
|
10109
|
+
* PHASE 0 — the harness's blocking human-question channel (`ctx.userQuestions`), when this composition
|
|
10110
|
+
* mounts one.
|
|
10111
|
+
*
|
|
10112
|
+
* ⚠ WHY THE PLUGIN REACHES FOR THIS AT ALL. The other three gates answer through a question card and a
|
|
10113
|
+
* relayed label, which is fine for a decision the workflow acts on later. STARTING A RUN is different:
|
|
10114
|
+
* the first human turn is the only place the harness can say "arming this goal means autonomous rounds"
|
|
10115
|
+
* BEFORE arming it. This channel is the same one plan-mode's exit uses; `ask()` resolves only with a
|
|
10116
|
+
* real answer from a real person, and it THROWS when there is no answerer or no live root agent. So the
|
|
10117
|
+
* absence of this service cannot be papered over: `recursive_ask` refuses the run-start gate and names
|
|
10118
|
+
* the missing channel (RM5502).
|
|
10119
|
+
*/
|
|
10120
|
+
userQuestions = null;
|
|
10121
|
+
/** Late-bind the human-question channel when the composition mounts it. */
|
|
10122
|
+
attachUserQuestions(service) {
|
|
10123
|
+
this.userQuestions = service;
|
|
10124
|
+
}
|
|
10125
|
+
/** The human-question channel this composition mounted, or null. */
|
|
10126
|
+
get userQuestionsChannel() {
|
|
10127
|
+
return this.userQuestions;
|
|
10128
|
+
}
|
|
10129
|
+
/**
|
|
9700
10130
|
* T1 (goals projection): project the run into the native goals service so it is
|
|
9701
10131
|
* a first-class durable, resumable, blockable object. Best-effort — the run's
|
|
9702
10132
|
* filesystem state is the source of truth; a goal is the durable projection.
|
|
10133
|
+
*
|
|
10134
|
+
* ⚠ PHASE 0 — AND IT ARMS NOTHING UNLESS THE RUN WAS STARTED. `create` returns an ARMED goal, and an
|
|
10135
|
+
* armed goal is the harness driving autonomous rounds, so this is the one place where "project the
|
|
10136
|
+
* state" can quietly equal "start the run". The approval is therefore REQUIRED from the caller and
|
|
10137
|
+
* has no default here: a caller that has not resolved the run's approval cannot arm a goal by
|
|
10138
|
+
* forgetting to pass one, and `syncRunGoal` refuses every branch that would create one without it.
|
|
10139
|
+
*
|
|
10140
|
+
* Unapproved is the EXPECTED state for a scaffolded run, so the refusal comes back as a plain
|
|
10141
|
+
* `{ ok: false }` carrying {@link RUN_START_NOT_APPROVED}: callers must treat that as normal work,
|
|
10142
|
+
* never as a warning (see `armRunGoalIfApproved`, the one caller that arms).
|
|
10143
|
+
*/
|
|
10144
|
+
projectRunToGoal(agent, runId, state = "active", approved = false) {
|
|
10145
|
+
if (!agent) return {
|
|
10146
|
+
ok: false,
|
|
10147
|
+
reason: "no agent"
|
|
10148
|
+
};
|
|
10149
|
+
return syncRunGoal(this.goalsService, agent, runId, state, approved);
|
|
10150
|
+
}
|
|
10151
|
+
/**
|
|
10152
|
+
* PHASE 0 — the approved-run arm step: read the run's approval from `root` and project the goal only
|
|
10153
|
+
* if it is there. This is the phase-progress path (`syncRunGoal` reached on ordinary work), so the
|
|
10154
|
+
* unapproved case is deliberately silent: `{ ok: false, reason: RUN_START_NOT_APPROVED }` with no
|
|
10155
|
+
* goal, no write and no throw. Read on EVERY call rather than cached, because the approval can arrive
|
|
10156
|
+
* mid-session and a cached "not yet" would leave an approved run unable to arm until a plugin reload.
|
|
9703
10157
|
*/
|
|
9704
|
-
|
|
10158
|
+
armRunGoalIfApproved(agent, root, runId, state = "active") {
|
|
9705
10159
|
if (!agent) return {
|
|
9706
10160
|
ok: false,
|
|
9707
10161
|
reason: "no agent"
|
|
9708
10162
|
};
|
|
9709
|
-
|
|
10163
|
+
const approval = this.readRunStartApproval(root, runId);
|
|
10164
|
+
return syncRunGoal(this.goalsService, agent, runId, state, approval.approved);
|
|
9710
10165
|
}
|
|
9711
10166
|
/** T1: block the run's goal on a gate-block (durable + UI-visible). */
|
|
9712
10167
|
blockRunToGoal(agent, runId, reason) {
|
|
@@ -9716,13 +10171,18 @@ var RecursiveRuntime = class extends Service {
|
|
|
9716
10171
|
};
|
|
9717
10172
|
return blockRunGoal(this.goalsService, agent, runId, reason);
|
|
9718
10173
|
}
|
|
9719
|
-
/**
|
|
9720
|
-
|
|
10174
|
+
/**
|
|
10175
|
+
* T1: re-arm the run's goal on a reopen (blocked/paused -> active). Never starts an unstarted run —
|
|
10176
|
+
* REOPEN IS NOT A BACK DOOR TO STARTING A RUN. `approved` is required for the same reason as in
|
|
10177
|
+
* `projectRunToGoal`: the phase-0 gate cannot be defaulted open. An approved run's approval outlives
|
|
10178
|
+
* a reopen because it is a durable line in the run's own Phase 0 artifact, not a held value.
|
|
10179
|
+
*/
|
|
10180
|
+
resumeRunToGoal(agent, runId, approved = false) {
|
|
9721
10181
|
if (!agent) return {
|
|
9722
10182
|
ok: false,
|
|
9723
10183
|
reason: "no agent"
|
|
9724
10184
|
};
|
|
9725
|
-
return resumeRunGoal(this.goalsService, agent, runId);
|
|
10185
|
+
return resumeRunGoal(this.goalsService, agent, runId, approved);
|
|
9726
10186
|
}
|
|
9727
10187
|
/**
|
|
9728
10188
|
* Workspace-scoped control-plane root (R1 binding invariant).
|
|
@@ -10372,15 +10832,61 @@ var RecursiveRuntime = class extends Service {
|
|
|
10372
10832
|
runDir,
|
|
10373
10833
|
runId,
|
|
10374
10834
|
created,
|
|
10375
|
-
existing
|
|
10835
|
+
existing,
|
|
10836
|
+
runStartApproval: {
|
|
10837
|
+
...this.readRunStartApproval(scaffoldRoot, runId),
|
|
10838
|
+
gate: RUN_START_GATE_ID
|
|
10839
|
+
}
|
|
10376
10840
|
};
|
|
10377
10841
|
if (worktree) result.worktree = worktree;
|
|
10378
|
-
try {
|
|
10379
|
-
this.projectRunToGoal(agent, runId, "active");
|
|
10380
|
-
} catch {}
|
|
10381
10842
|
return result;
|
|
10382
10843
|
}
|
|
10383
10844
|
/**
|
|
10845
|
+
* PHASE 0 — THE APPROVAL ACT: record the human's `Start run` decision and arm the run's goal.
|
|
10846
|
+
*
|
|
10847
|
+
* ⚠ THE ONLY PATH THAT STARTS A RUN. It exists as one method rather than as "write a line, then
|
|
10848
|
+
* project the goal" at the tool, because those two steps must not be separable: an approval recorded
|
|
10849
|
+
* without the arm (or an arm without the record) is exactly the half-state that made this defect hard
|
|
10850
|
+
* to see. `tests/run-start-approval.spec.ts` drives both halves through this one call.
|
|
10851
|
+
*
|
|
10852
|
+
* The approval line goes into the run's own Phase 0 artifact, so it is durable, citable, and survives
|
|
10853
|
+
* the session — and so a reader of the run can answer "was this run started, and by what?" without the
|
|
10854
|
+
* transcript. `answer` is validated against the gate's own labels before it reaches here.
|
|
10855
|
+
*/
|
|
10856
|
+
approveRunStart(root, runId, agent, answer = RUN_START_APPROVE) {
|
|
10857
|
+
if (root.trim() === "" || runId.trim() === "") return {
|
|
10858
|
+
ok: false,
|
|
10859
|
+
reason: "a run start needs a workspace root and a run id",
|
|
10860
|
+
path: "",
|
|
10861
|
+
replaced: false,
|
|
10862
|
+
goal: {
|
|
10863
|
+
ok: false,
|
|
10864
|
+
reason: "no run to start"
|
|
10865
|
+
}
|
|
10866
|
+
};
|
|
10867
|
+
runStartArtifactPath(root, runId);
|
|
10868
|
+
const written = this.recordAskAnswer(root, runId, RUN_START_ARTIFACT, "- Run Start: " + answer);
|
|
10869
|
+
const approval = this.readRunStartApproval(root, runId);
|
|
10870
|
+
if (!approval.approved) return {
|
|
10871
|
+
ok: false,
|
|
10872
|
+
reason: approval.reason,
|
|
10873
|
+
path: written.path,
|
|
10874
|
+
replaced: written.replaced,
|
|
10875
|
+
goal: {
|
|
10876
|
+
ok: false,
|
|
10877
|
+
reason: RUN_START_NOT_APPROVED
|
|
10878
|
+
}
|
|
10879
|
+
};
|
|
10880
|
+
const goal = this.armRunGoalIfApproved(agent, root, runId, "active");
|
|
10881
|
+
return {
|
|
10882
|
+
ok: true,
|
|
10883
|
+
reason: "",
|
|
10884
|
+
path: written.path,
|
|
10885
|
+
replaced: written.replaced,
|
|
10886
|
+
goal
|
|
10887
|
+
};
|
|
10888
|
+
}
|
|
10889
|
+
/**
|
|
10384
10890
|
* Create a linked worktree for a run under the given workspace root. The
|
|
10385
10891
|
* worktree branch defaults to `recursive/<runId>` and is cut from the given
|
|
10386
10892
|
* base branch (default: the current HEAD branch of the root checkout).
|
|
@@ -10443,6 +10949,8 @@ var RecursiveRuntime = class extends Service {
|
|
|
10443
10949
|
}
|
|
10444
10950
|
const inFlight = pendingWork(runDir);
|
|
10445
10951
|
if (inFlight.length > 0) throw new Error(toolError("PENDING_WORK", inFlight.map((p) => p.detail).join("; ")));
|
|
10952
|
+
const lint = await this.lintArtifact(runId, artifact, agent);
|
|
10953
|
+
if (!lint.passed) throw new Error("Artifact " + artifact + " does not meet the phase standard, so it was not locked: " + lint.errors.join("; "));
|
|
10446
10954
|
let content = readFileSync(artifactPath, "utf8");
|
|
10447
10955
|
const lockedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
10448
10956
|
content = setOrInsertField(content, "Status", "LOCKED", ["Phase"]);
|
|
@@ -10487,7 +10995,7 @@ var RecursiveRuntime = class extends Service {
|
|
|
10487
10995
|
const stale = getStaleDownstreamPhases(runDir, artifact);
|
|
10488
10996
|
for (const entry of stale) invalidateReceipt(runDir, entry.artifact);
|
|
10489
10997
|
try {
|
|
10490
|
-
this.resumeRunToGoal(agent, runId);
|
|
10998
|
+
this.resumeRunToGoal(agent, runId, this.readRunStartApproval(root, runId).approved);
|
|
10491
10999
|
} catch {}
|
|
10492
11000
|
return {
|
|
10493
11001
|
artifact,
|
|
@@ -10840,10 +11348,25 @@ function createRecursiveStatusTool(recursive) {
|
|
|
10840
11348
|
}
|
|
10841
11349
|
//#endregion
|
|
10842
11350
|
//#region src/recursive_init.tool.ts
|
|
11351
|
+
/**
|
|
11352
|
+
* PHASE 0 — SCAFFOLDING IS NOT STARTING, AND THE TOOL SAYS SO AT THE MOMENT IT MATTERS.
|
|
11353
|
+
*
|
|
11354
|
+
* A spec may legitimately exist before a run does: this tool writes the run directory and every phase
|
|
11355
|
+
* document, and it still does. What it must NOT do is start the run, because starting is creating and
|
|
11356
|
+
* arming the goal the harness drives autonomous rounds from. That is the owner's rule — *"phase 0
|
|
11357
|
+
* requires explicit approval to start a run and goal"* — so the description below names the gate and the
|
|
11358
|
+
* result carries `runStartApproval`, which is the pointer a caller needs: the run is inert until
|
|
11359
|
+
* `recursive_ask` answers `run-start`.
|
|
11360
|
+
*
|
|
11361
|
+
* `runStartApproval` is read from the run's own Phase 0 artifact on every call, so it is the TRUE state
|
|
11362
|
+
* rather than "this call created something": re-initialising an APPROVED run reports `approved: true`
|
|
11363
|
+
* (and the run keeps its goal), which is what a caller re-scaffolding a run it already started needs to
|
|
11364
|
+
* see.
|
|
11365
|
+
*/
|
|
10843
11366
|
function createRecursiveInitTool(recursive) {
|
|
10844
11367
|
return defineTool({
|
|
10845
11368
|
name: "recursive_init",
|
|
10846
|
-
description: "Scaffold a new recursive-mode run directory (or ensure an existing one) with stub artifact headers. Delegates to the RecursiveRuntime service (no duplicated scaffolding logic). When createWorktree is true, a linked worktree is created first and the run is scaffolded inside it.",
|
|
11369
|
+
description: "Scaffold a new recursive-mode run directory (or ensure an existing one) with stub artifact headers. Delegates to the RecursiveRuntime service (no duplicated scaffolding logic). When createWorktree is true, a linked worktree is created first and the run is scaffolded inside it. THIS DOES NOT START THE RUN: no goal exists until the user approves phase 0 through recursive_ask gate=run-start, so the result carries runStartApproval — read it and ask.",
|
|
10847
11370
|
parameters: {
|
|
10848
11371
|
runId: {
|
|
10849
11372
|
type: "string",
|
|
@@ -12526,7 +13049,7 @@ function renderExplained(entries) {
|
|
|
12526
13049
|
* present-but-empty value would stop the deferral and pin the choice to nothing, which is a different state
|
|
12527
13050
|
* from "unconfigured" and not one a user can see. Absence is the honest representation of "I have no opinion".
|
|
12528
13051
|
*
|
|
12529
|
-
* 3. **WRITE ATOMICALLY.** A temp file and a rename, the same pattern the preset installer
|
|
13052
|
+
* 3. **WRITE ATOMICALLY.** A temp file and a rename, the same pattern the preset installer used, before it was retired. A policy file
|
|
12530
13053
|
* half-written because a process died mid-write would make `loadRouterPolicy` fall back to the built-in
|
|
12531
13054
|
* self-audit policy — and it does that SILENTLY, on purpose (it never throws). So a torn write here would look
|
|
12532
13055
|
* exactly like "the user configured nothing", for every role, until someone read the file.
|
|
@@ -13590,6 +14113,10 @@ function apply(ctx, config) {
|
|
|
13590
14113
|
ctx.inject(["llm"], (llmCtx) => {
|
|
13591
14114
|
recursive.attachLlmInventory(llmCtx.get("llm") ?? null);
|
|
13592
14115
|
});
|
|
14116
|
+
recursive.attachUserQuestions(ctx.get("userQuestions") ?? null);
|
|
14117
|
+
ctx.inject(["userQuestions"], (questionsCtx) => {
|
|
14118
|
+
recursive.attachUserQuestions(questionsCtx.get("userQuestions") ?? null);
|
|
14119
|
+
});
|
|
13593
14120
|
if (config?.enforcement !== void 0) recursive.setEnforcementConfig(config.enforcement);
|
|
13594
14121
|
const phaseSkills = registerPhaseSkills(ctx.get("skills"), PHASE_SEQUENCE);
|
|
13595
14122
|
yield () => {
|
|
@@ -13614,9 +14141,17 @@ function apply(ctx, config) {
|
|
|
13614
14141
|
ctx.tools.register(createRecursiveReviewTool(recursive, subagentsSeam)),
|
|
13615
14142
|
ctx.tools.register(createRecursiveDelegateTool(recursive, subagentsSeam)),
|
|
13616
14143
|
ctx.tools.register(createRecursiveAskTool(recursive)),
|
|
13617
|
-
ctx.tools.register(createRecursivePreviewTool(recursive))
|
|
13618
|
-
...agentTeams ? [ctx.tools.register(createRecursiveAuditTeamTool(agentTeams))] : []
|
|
14144
|
+
ctx.tools.register(createRecursivePreviewTool(recursive))
|
|
13619
14145
|
];
|
|
14146
|
+
let auditTeamRegistered = agentTeams !== void 0 && agentTeams !== null;
|
|
14147
|
+
if (auditTeamRegistered) disposers.push(ctx.tools.register(createRecursiveAuditTeamTool(agentTeams ?? null)));
|
|
14148
|
+
ctx.inject(["agentTeams"], (teamCtx) => {
|
|
14149
|
+
if (auditTeamRegistered) return;
|
|
14150
|
+
const late = teamCtx.get("agentTeams");
|
|
14151
|
+
if (late === void 0 || late === null) return;
|
|
14152
|
+
auditTeamRegistered = true;
|
|
14153
|
+
ctx.tools.register(createRecursiveAuditTeamTool(late));
|
|
14154
|
+
});
|
|
13620
14155
|
const commands = ctx.get("commands");
|
|
13621
14156
|
if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
|
|
13622
14157
|
const systemPrompt = ctx.get("systemPrompt");
|
|
@@ -13715,10 +14250,10 @@ function apply(ctx, config) {
|
|
|
13715
14250
|
if (observation?.kind !== "present") return;
|
|
13716
14251
|
const displayPath = target?.displayPath ?? "";
|
|
13717
14252
|
if (!displayPath) return;
|
|
13718
|
-
const normalized = displayPath.replace(/\\/g, "/");
|
|
13719
|
-
if (!normalized.endsWith(".md") || !normalized.includes("/.recursive/run/")) return;
|
|
13720
14253
|
const cwd = actor?.agent?.session?.header?.cwd ?? "";
|
|
13721
14254
|
if (!cwd) return;
|
|
14255
|
+
const normalized = displayPath.replace(/\\/g, "/");
|
|
14256
|
+
if (!tamperCandidatePath(normalized, cwd)) return;
|
|
13722
14257
|
const runId = resolveRunDir(cwd)?.runId ?? "";
|
|
13723
14258
|
const tamper = recursive.detectTamper(normalized, cwd, runId);
|
|
13724
14259
|
if (!tamper) return;
|
|
@@ -13823,4 +14358,4 @@ function apply(ctx, config) {
|
|
|
13823
14358
|
});
|
|
13824
14359
|
}
|
|
13825
14360
|
//#endregion
|
|
13826
|
-
export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
|
14361
|
+
export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, tamperCandidatePath, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|