@try-works/dsh-recursive-mode 0.4.4 → 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/lib/errors.d.ts +12 -0
- package/lib/goals-projection.d.ts +34 -4
- package/lib/index.js +471 -41
- 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/errors.ts +12 -0
- package/src/goals-projection.ts +48 -7
- package/src/index.ts +9 -0
- 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 +157 -14
package/lib/index.js
CHANGED
|
@@ -5462,6 +5462,18 @@ const TOOL_ERRORS = {
|
|
|
5462
5462
|
problem: "the run has unresolved delegated work, so this phase cannot lock yet",
|
|
5463
5463
|
next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again"
|
|
5464
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
|
+
},
|
|
5465
5477
|
RUNTIME_REFUSED: {
|
|
5466
5478
|
code: "RM5501",
|
|
5467
5479
|
klass: "runtime",
|
|
@@ -6819,6 +6831,123 @@ function extractAndGroup(runner, env, options = {}) {
|
|
|
6819
6831
|
};
|
|
6820
6832
|
}
|
|
6821
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
|
|
6822
6951
|
//#region src/recursive_ask.tool.ts
|
|
6823
6952
|
/**
|
|
6824
6953
|
* T23 — `recursive_ask`: the three human gates as STRUCTURED decisions, not prose.
|
|
@@ -6846,6 +6975,14 @@ const ASK_GATE_IDS = [
|
|
|
6846
6975
|
"qa-signoff",
|
|
6847
6976
|
"gate-block"
|
|
6848
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
|
+
}
|
|
6849
6986
|
const ASK_GATES = {
|
|
6850
6987
|
"tdd-mode": {
|
|
6851
6988
|
id: "tdd-mode",
|
|
@@ -6939,6 +7076,36 @@ function buildAskQuestion(gateId) {
|
|
|
6939
7076
|
});
|
|
6940
7077
|
}
|
|
6941
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
|
+
/**
|
|
6942
7109
|
* Validate an answer against its gate.
|
|
6943
7110
|
*
|
|
6944
7111
|
* An answer that is not one of the offered labels is REFUSED rather than recorded: a marker saying
|
|
@@ -6987,15 +7154,19 @@ function pendingGateFor(artifactFile, artifactText) {
|
|
|
6987
7154
|
* ⚠ THE WRITE-BACK IS A MARKER LINE, REPLACED IN PLACE when the artifact already carries one. A
|
|
6988
7155
|
* second `TDD Mode:` line would leave two answers to one question and make "what was decided?"
|
|
6989
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.
|
|
6990
7161
|
*/
|
|
6991
7162
|
function createRecursiveAskTool(recursive) {
|
|
6992
7163
|
return defineTool({
|
|
6993
7164
|
name: "recursive_ask",
|
|
6994
|
-
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.",
|
|
6995
7166
|
parameters: {
|
|
6996
7167
|
gate: {
|
|
6997
7168
|
type: "string",
|
|
6998
|
-
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."
|
|
6999
7170
|
},
|
|
7000
7171
|
runId: {
|
|
7001
7172
|
type: "string",
|
|
@@ -7003,11 +7174,11 @@ function createRecursiveAskTool(recursive) {
|
|
|
7003
7174
|
},
|
|
7004
7175
|
artifact: {
|
|
7005
7176
|
type: "string",
|
|
7006
|
-
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)."
|
|
7007
7178
|
},
|
|
7008
7179
|
answer: {
|
|
7009
7180
|
type: "string",
|
|
7010
|
-
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(" | ") + "."
|
|
7011
7182
|
}
|
|
7012
7183
|
},
|
|
7013
7184
|
output: {
|
|
@@ -7021,29 +7192,32 @@ function createRecursiveAskTool(recursive) {
|
|
|
7021
7192
|
const gateId = (args.gate ?? "").trim();
|
|
7022
7193
|
const runId = args.runId?.trim() ?? "";
|
|
7023
7194
|
if (runId === "") return { error: toolError("MISSING_RUN_ID") };
|
|
7024
|
-
if (!
|
|
7195
|
+
if (!askGateIds().includes(gateId)) return { error: toolError("BAD_ASK_GATE", "gate must be one of " + askGateIds().join(" | ")) };
|
|
7025
7196
|
const root = await recursive.resolveWorkspaceRoot(exec.agent);
|
|
7026
7197
|
if (!root) return { error: toolError("NO_WORKSPACE") };
|
|
7027
|
-
const artifact = (args.artifact ?? GATE_DEFAULT_ARTIFACT[gateId]).trim();
|
|
7198
|
+
const artifact = (isRunStartGate(gateId) ? RUN_START_ARTIFACT : args.artifact ?? GATE_DEFAULT_ARTIFACT[gateId]).trim();
|
|
7028
7199
|
let question;
|
|
7029
7200
|
try {
|
|
7030
|
-
question =
|
|
7201
|
+
question = buildAskQuestionFor(gateId);
|
|
7031
7202
|
} catch (err) {
|
|
7032
7203
|
return { error: toolError("BAD_ASK_GATE", err instanceof Error ? err.message : String(err)) };
|
|
7033
7204
|
}
|
|
7034
|
-
|
|
7205
|
+
const channelMounted = recursive.userQuestionsChannel !== null;
|
|
7206
|
+
if (args.answer === void 0 && !(isRunStartGate(gateId) && channelMounted)) return {
|
|
7035
7207
|
gate: gateId,
|
|
7036
|
-
marker: ASK_GATES[gateId].marker,
|
|
7208
|
+
marker: isRunStartGate(gateId) ? RUN_START_GATE.marker : ASK_GATES[gateId].marker,
|
|
7037
7209
|
artifact,
|
|
7038
7210
|
question
|
|
7039
7211
|
};
|
|
7040
7212
|
let answer;
|
|
7041
|
-
|
|
7042
|
-
|
|
7213
|
+
if (args.answer === void 0) answer = void 0;
|
|
7214
|
+
else try {
|
|
7215
|
+
answer = validateAskAnswerFor(gateId, args.answer);
|
|
7043
7216
|
} catch (err) {
|
|
7044
7217
|
return { error: toolError("BAD_ASK_ANSWER", err instanceof Error ? err.message : String(err)) };
|
|
7045
7218
|
}
|
|
7046
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);
|
|
7047
7221
|
const marker = answerMarker(gateId, answer);
|
|
7048
7222
|
const written = recursive.recordAskAnswer(root, runId, artifact, marker);
|
|
7049
7223
|
return {
|
|
@@ -7057,6 +7231,76 @@ function createRecursiveAskTool(recursive) {
|
|
|
7057
7231
|
}
|
|
7058
7232
|
});
|
|
7059
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
|
+
}
|
|
7060
7304
|
//#endregion
|
|
7061
7305
|
//#region src/lifecycle.ts
|
|
7062
7306
|
/**
|
|
@@ -8911,8 +9155,25 @@ function mutatePhase(service, agent, ref, target) {
|
|
|
8911
9155
|
* Sync a run's durable goal to the requested phase. Safe: never touches a goal
|
|
8912
9156
|
* whose objective is not this run's marker, and never re-creates over a
|
|
8913
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`).
|
|
8914
9175
|
*/
|
|
8915
|
-
function syncRunGoal(service, agent, runId, runState) {
|
|
9176
|
+
function syncRunGoal(service, agent, runId, runState, approved = false) {
|
|
8916
9177
|
if (!service) return {
|
|
8917
9178
|
ok: false,
|
|
8918
9179
|
reason: "no goals service"
|
|
@@ -8927,12 +9188,18 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8927
9188
|
phase: target,
|
|
8928
9189
|
ref
|
|
8929
9190
|
};
|
|
8930
|
-
if (phase === "complete")
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
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
|
+
}
|
|
8936
9203
|
return mutatePhase(service, agent, ref, target) ? {
|
|
8937
9204
|
ok: true,
|
|
8938
9205
|
phase: target,
|
|
@@ -8943,17 +9210,27 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8943
9210
|
};
|
|
8944
9211
|
}
|
|
8945
9212
|
if (current) {
|
|
8946
|
-
if (current.phase === "complete")
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
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
|
+
}
|
|
8952
9225
|
return {
|
|
8953
9226
|
ok: false,
|
|
8954
9227
|
reason: "a non-matching active goal exists (foreign goal not touched)"
|
|
8955
9228
|
};
|
|
8956
9229
|
}
|
|
9230
|
+
if (!approved) return {
|
|
9231
|
+
ok: false,
|
|
9232
|
+
reason: RUN_START_NOT_APPROVED
|
|
9233
|
+
};
|
|
8957
9234
|
return {
|
|
8958
9235
|
ok: true,
|
|
8959
9236
|
phase: target,
|
|
@@ -8961,7 +9238,13 @@ function syncRunGoal(service, agent, runId, runState) {
|
|
|
8961
9238
|
created: true
|
|
8962
9239
|
};
|
|
8963
9240
|
}
|
|
8964
|
-
/**
|
|
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
|
+
*/
|
|
8965
9248
|
function blockRunGoal(service, agent, runId, reason) {
|
|
8966
9249
|
if (!service) return {
|
|
8967
9250
|
ok: false,
|
|
@@ -8970,7 +9253,7 @@ function blockRunGoal(service, agent, runId, reason) {
|
|
|
8970
9253
|
const current = service.get(agent);
|
|
8971
9254
|
if (!current) return {
|
|
8972
9255
|
ok: false,
|
|
8973
|
-
reason:
|
|
9256
|
+
reason: RUN_START_NOT_APPROVED
|
|
8974
9257
|
};
|
|
8975
9258
|
if (!isRunGoal(current, runId)) return {
|
|
8976
9259
|
ok: false,
|
|
@@ -8986,9 +9269,16 @@ function blockRunGoal(service, agent, runId, reason) {
|
|
|
8986
9269
|
reason: "goal block failed"
|
|
8987
9270
|
};
|
|
8988
9271
|
}
|
|
8989
|
-
/**
|
|
8990
|
-
|
|
8991
|
-
|
|
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);
|
|
8992
9282
|
}
|
|
8993
9283
|
//#endregion
|
|
8994
9284
|
//#region src/teams-loop.ts
|
|
@@ -9598,9 +9888,23 @@ var RecursiveRuntime = class extends Service {
|
|
|
9598
9888
|
this.subagentsSeam = config.subagents ?? null;
|
|
9599
9889
|
this.workflow = config.workflow ?? null;
|
|
9600
9890
|
}
|
|
9601
|
-
/**
|
|
9891
|
+
/**
|
|
9892
|
+
* T10: the native jobs registry, when the composition mounts one. */
|
|
9602
9893
|
jobs;
|
|
9603
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
|
+
/**
|
|
9604
9908
|
* T23 — write a gate's answer into an artifact as a marker line.
|
|
9605
9909
|
*
|
|
9606
9910
|
* ⚠ REPLACED IN PLACE when the artifact already carries that gate's marker: two `TDD Mode:` lines
|
|
@@ -9792,16 +10096,72 @@ var RecursiveRuntime = class extends Service {
|
|
|
9792
10096
|
return report;
|
|
9793
10097
|
}
|
|
9794
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
|
+
/**
|
|
9795
10130
|
* T1 (goals projection): project the run into the native goals service so it is
|
|
9796
10131
|
* a first-class durable, resumable, blockable object. Best-effort — the run's
|
|
9797
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).
|
|
9798
10143
|
*/
|
|
9799
|
-
projectRunToGoal(agent, runId, state = "active") {
|
|
10144
|
+
projectRunToGoal(agent, runId, state = "active", approved = false) {
|
|
9800
10145
|
if (!agent) return {
|
|
9801
10146
|
ok: false,
|
|
9802
10147
|
reason: "no agent"
|
|
9803
10148
|
};
|
|
9804
|
-
return syncRunGoal(this.goalsService, agent, runId, state);
|
|
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.
|
|
10157
|
+
*/
|
|
10158
|
+
armRunGoalIfApproved(agent, root, runId, state = "active") {
|
|
10159
|
+
if (!agent) return {
|
|
10160
|
+
ok: false,
|
|
10161
|
+
reason: "no agent"
|
|
10162
|
+
};
|
|
10163
|
+
const approval = this.readRunStartApproval(root, runId);
|
|
10164
|
+
return syncRunGoal(this.goalsService, agent, runId, state, approval.approved);
|
|
9805
10165
|
}
|
|
9806
10166
|
/** T1: block the run's goal on a gate-block (durable + UI-visible). */
|
|
9807
10167
|
blockRunToGoal(agent, runId, reason) {
|
|
@@ -9811,13 +10171,18 @@ var RecursiveRuntime = class extends Service {
|
|
|
9811
10171
|
};
|
|
9812
10172
|
return blockRunGoal(this.goalsService, agent, runId, reason);
|
|
9813
10173
|
}
|
|
9814
|
-
/**
|
|
9815
|
-
|
|
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) {
|
|
9816
10181
|
if (!agent) return {
|
|
9817
10182
|
ok: false,
|
|
9818
10183
|
reason: "no agent"
|
|
9819
10184
|
};
|
|
9820
|
-
return resumeRunGoal(this.goalsService, agent, runId);
|
|
10185
|
+
return resumeRunGoal(this.goalsService, agent, runId, approved);
|
|
9821
10186
|
}
|
|
9822
10187
|
/**
|
|
9823
10188
|
* Workspace-scoped control-plane root (R1 binding invariant).
|
|
@@ -10467,15 +10832,61 @@ var RecursiveRuntime = class extends Service {
|
|
|
10467
10832
|
runDir,
|
|
10468
10833
|
runId,
|
|
10469
10834
|
created,
|
|
10470
|
-
existing
|
|
10835
|
+
existing,
|
|
10836
|
+
runStartApproval: {
|
|
10837
|
+
...this.readRunStartApproval(scaffoldRoot, runId),
|
|
10838
|
+
gate: RUN_START_GATE_ID
|
|
10839
|
+
}
|
|
10471
10840
|
};
|
|
10472
10841
|
if (worktree) result.worktree = worktree;
|
|
10473
|
-
try {
|
|
10474
|
-
this.projectRunToGoal(agent, runId, "active");
|
|
10475
|
-
} catch {}
|
|
10476
10842
|
return result;
|
|
10477
10843
|
}
|
|
10478
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
|
+
/**
|
|
10479
10890
|
* Create a linked worktree for a run under the given workspace root. The
|
|
10480
10891
|
* worktree branch defaults to `recursive/<runId>` and is cut from the given
|
|
10481
10892
|
* base branch (default: the current HEAD branch of the root checkout).
|
|
@@ -10584,7 +10995,7 @@ var RecursiveRuntime = class extends Service {
|
|
|
10584
10995
|
const stale = getStaleDownstreamPhases(runDir, artifact);
|
|
10585
10996
|
for (const entry of stale) invalidateReceipt(runDir, entry.artifact);
|
|
10586
10997
|
try {
|
|
10587
|
-
this.resumeRunToGoal(agent, runId);
|
|
10998
|
+
this.resumeRunToGoal(agent, runId, this.readRunStartApproval(root, runId).approved);
|
|
10588
10999
|
} catch {}
|
|
10589
11000
|
return {
|
|
10590
11001
|
artifact,
|
|
@@ -10937,10 +11348,25 @@ function createRecursiveStatusTool(recursive) {
|
|
|
10937
11348
|
}
|
|
10938
11349
|
//#endregion
|
|
10939
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
|
+
*/
|
|
10940
11366
|
function createRecursiveInitTool(recursive) {
|
|
10941
11367
|
return defineTool({
|
|
10942
11368
|
name: "recursive_init",
|
|
10943
|
-
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.",
|
|
10944
11370
|
parameters: {
|
|
10945
11371
|
runId: {
|
|
10946
11372
|
type: "string",
|
|
@@ -13687,6 +14113,10 @@ function apply(ctx, config) {
|
|
|
13687
14113
|
ctx.inject(["llm"], (llmCtx) => {
|
|
13688
14114
|
recursive.attachLlmInventory(llmCtx.get("llm") ?? null);
|
|
13689
14115
|
});
|
|
14116
|
+
recursive.attachUserQuestions(ctx.get("userQuestions") ?? null);
|
|
14117
|
+
ctx.inject(["userQuestions"], (questionsCtx) => {
|
|
14118
|
+
recursive.attachUserQuestions(questionsCtx.get("userQuestions") ?? null);
|
|
14119
|
+
});
|
|
13690
14120
|
if (config?.enforcement !== void 0) recursive.setEnforcementConfig(config.enforcement);
|
|
13691
14121
|
const phaseSkills = registerPhaseSkills(ctx.get("skills"), PHASE_SEQUENCE);
|
|
13692
14122
|
yield () => {
|