@try-works/dsh-recursive-mode 0.4.6 → 0.4.8
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 +11 -4
- package/lib/client/contract.d.ts +10 -0
- package/lib/client/doc-viewer.d.ts +50 -0
- package/lib/client/spec-sheet-view.d.ts +237 -0
- package/lib/client/spec-sheet.d.ts +103 -0
- package/lib/client/use-live.d.ts +17 -1
- package/lib/client.js +988 -14
- package/lib/delegation.d.ts +35 -1
- package/lib/errors.d.ts +13 -0
- package/lib/index.js +164 -9
- package/lib/run-spec.d.ts +78 -0
- package/lib/run-start.d.ts +27 -0
- package/package.json +1 -1
- package/src/client/contract.ts +10 -0
- package/src/client/doc-viewer.tsx +131 -12
- package/src/client/slots.ts +12 -0
- package/src/client/spec-sheet-view.ts +325 -0
- package/src/client/spec-sheet.tsx +409 -0
- package/src/client/styles.ts +298 -1
- package/src/client/use-live.ts +23 -2
- package/src/delegation.ts +58 -4
- package/src/errors.ts +13 -0
- package/src/recursive_ask.tool.ts +28 -1
- package/src/run-spec.ts +148 -0
- package/src/run-start.ts +59 -0
- package/src/runtime.ts +78 -24
package/lib/delegation.d.ts
CHANGED
|
@@ -336,9 +336,40 @@ export interface ActionRecordInput {
|
|
|
336
336
|
* nothing else: a delegation that FAILED and a delegation that NEVER HAPPENED read identically, which is what
|
|
337
337
|
* let me conclude for three rounds that the host was not scheduling children. The caller ALREADY passed
|
|
338
338
|
* `stopReason`, and the live record said `n/a` — because there was no result to take a stop reason from.
|
|
339
|
+
*
|
|
340
|
+
* ⚠ AND IT IS EMITTED ONLY FOR A GENUINE FAILURE. A PARKED round is neither accepted nor failed, so it
|
|
341
|
+
* carries {@link ActionRecordInput.parked} instead: calling a live child a failure is the defect this
|
|
342
|
+
* field's own history is made of.
|
|
339
343
|
*/
|
|
340
344
|
failure?: string;
|
|
345
|
+
/**
|
|
346
|
+
* ⚠ THE THIRD STATE, AND WHY `success` COULD NOT CARRY IT.
|
|
347
|
+
*
|
|
348
|
+
* A continuable round that has not settled is NOT a failure — `ContinuableDelegationLike.parked` says so in
|
|
349
|
+
* its own doc comment, and the tool result already surfaces it. The RECORD did not: a live run's child was
|
|
350
|
+
* parked, the record said `Status: failed` with "the child never reported, or never ran", the main agent
|
|
351
|
+
* read that as a dead child and obtained the review elsewhere — while the child was still working and
|
|
352
|
+
* replied eighteen minutes later. `success: false` cannot express "still in flight", because a genuinely
|
|
353
|
+
* dead child also produces `success: false`, so a second field is required rather than a cleverer boolean.
|
|
354
|
+
*
|
|
355
|
+
* When true, the record says `parked` and carries a `Parked:` line (never a `Failure:` line) whose text
|
|
356
|
+
* states only what is KNOWN, names the `childId`, and names the resume step. It takes precedence over
|
|
357
|
+
* `success`: an unobserved round is never an acceptance, whatever a caller passes alongside it.
|
|
358
|
+
*/
|
|
359
|
+
parked?: boolean;
|
|
341
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* The status a record states, in ONE place, because the three states are the fix and a second writer would
|
|
363
|
+
* drift from this one.
|
|
364
|
+
*
|
|
365
|
+
* The wording is chosen for two readers at once. The MODEL reads it to decide whether to resume or to give up
|
|
366
|
+
* and obtain the result another way — the exact decision the live defect got wrong — so the token must not
|
|
367
|
+
* read as a failure and must not need the rest of the document to be understood. A HUMAN reading the run tree
|
|
368
|
+
* months later needs to tell "died" from "still working" at a glance. Hence `parked (still running; no
|
|
369
|
+
* settlement yet)`: a third token rather than a renamed second, qualified with the two facts that separate it
|
|
370
|
+
* from `failed`, and short enough to sit in a status line.
|
|
371
|
+
*/
|
|
372
|
+
export declare function actionRecordStatus(input: Pick<ActionRecordInput, 'success' | 'parked'>): string;
|
|
342
373
|
/**
|
|
343
374
|
* Write a durable action record under subagents/ in the shape this repo's own
|
|
344
375
|
* linter accepts (ts-lint.ts lintSubagentActionRecordFile — every top-level .md
|
|
@@ -351,7 +382,10 @@ export interface ActionRecordInput {
|
|
|
351
382
|
* `Code Refs` strictly inside ## Inputs Provided. The linter resolves each of
|
|
352
383
|
* those through the heading body, so a field under another heading is not
|
|
353
384
|
* found at all.
|
|
354
|
-
* A success:false attempt is written with a failed status and is NOT accepted.
|
|
385
|
+
* A success:false attempt is written with a failed status and is NOT accepted. A round that PARKED is written
|
|
386
|
+
* with a status of its own (`parked (still running; no settlement yet)`, see {@link actionRecordStatus}) and a
|
|
387
|
+
* `Parked:` line instead of a `Failure:` one, because no settlement is not a death: it is the caller's signal to
|
|
388
|
+
* resume the same child. Only a genuine failure carries `Failure:`.
|
|
355
389
|
*/
|
|
356
390
|
export declare function writeActionRecord(input: ActionRecordInput): string;
|
|
357
391
|
/**
|
package/lib/errors.d.ts
CHANGED
|
@@ -151,6 +151,19 @@ export declare const TOOL_ERRORS: {
|
|
|
151
151
|
readonly problem: "the run has unresolved delegated work, so this phase cannot lock yet";
|
|
152
152
|
readonly next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again";
|
|
153
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* THE ORDERING DEFECT, AS A CODE. `recursive_ask gate=run-start` used to raise "start this run or hold?"
|
|
156
|
+
* over a Phase 0 document that was still the scaffold `recursive_init` wrote — placeholder requirements,
|
|
157
|
+
* unchecked lists, `FAIL` gates — and nothing put that document in front of the person either. The owner:
|
|
158
|
+
* *"i was never shown the spec before that so how could i approve if i havent seen it"*. Approving an
|
|
159
|
+
* unfilled template is not a decision about a spec, so the gate refuses to be raised until there is one.
|
|
160
|
+
*/
|
|
161
|
+
readonly RUN_START_SPEC_UNFILLED: {
|
|
162
|
+
readonly code: "RM4404";
|
|
163
|
+
readonly klass: "state";
|
|
164
|
+
readonly problem: "the Phase 0 requirements document is still the unfilled template, so there is no run spec for a person to approve";
|
|
165
|
+
readonly next: "fill the requirements document in (define the requirement ids and their acceptance criteria, and complete the TODO list) and then call recursive_ask with gate: run-start again";
|
|
166
|
+
};
|
|
154
167
|
readonly RUN_START_NO_CHANNEL: {
|
|
155
168
|
readonly code: "RM5502";
|
|
156
169
|
readonly klass: "runtime";
|
package/lib/index.js
CHANGED
|
@@ -5480,6 +5480,19 @@ const TOOL_ERRORS = {
|
|
|
5480
5480
|
problem: "the run has unresolved delegated work, so this phase cannot lock yet",
|
|
5481
5481
|
next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again"
|
|
5482
5482
|
},
|
|
5483
|
+
/**
|
|
5484
|
+
* THE ORDERING DEFECT, AS A CODE. `recursive_ask gate=run-start` used to raise "start this run or hold?"
|
|
5485
|
+
* over a Phase 0 document that was still the scaffold `recursive_init` wrote — placeholder requirements,
|
|
5486
|
+
* unchecked lists, `FAIL` gates — and nothing put that document in front of the person either. The owner:
|
|
5487
|
+
* *"i was never shown the spec before that so how could i approve if i havent seen it"*. Approving an
|
|
5488
|
+
* unfilled template is not a decision about a spec, so the gate refuses to be raised until there is one.
|
|
5489
|
+
*/
|
|
5490
|
+
RUN_START_SPEC_UNFILLED: {
|
|
5491
|
+
code: "RM4404",
|
|
5492
|
+
klass: "state",
|
|
5493
|
+
problem: "the Phase 0 requirements document is still the unfilled template, so there is no run spec for a person to approve",
|
|
5494
|
+
next: "fill the requirements document in (define the requirement ids and their acceptance criteria, and complete the TODO list) and then call recursive_ask with gate: run-start again"
|
|
5495
|
+
},
|
|
5483
5496
|
RUN_START_NO_CHANNEL: {
|
|
5484
5497
|
code: "RM5502",
|
|
5485
5498
|
klass: "runtime",
|
|
@@ -6864,6 +6877,79 @@ function extractAndGroup(runner, env, options = {}) {
|
|
|
6864
6877
|
};
|
|
6865
6878
|
}
|
|
6866
6879
|
//#endregion
|
|
6880
|
+
//#region src/run-spec.ts
|
|
6881
|
+
/** The named evidence classes, so a reader can tell a placeholder from an unmet gate. */
|
|
6882
|
+
const ARTIFACT_MARKER_IDS = {
|
|
6883
|
+
placeholder: "placeholder",
|
|
6884
|
+
uncheckedTodo: "unchecked-todo",
|
|
6885
|
+
failedGate: "failed-gate"
|
|
6886
|
+
};
|
|
6887
|
+
/** Every marker, in the order they are reported for a single line. */
|
|
6888
|
+
const MARKER_PATTERNS = [
|
|
6889
|
+
{
|
|
6890
|
+
id: ARTIFACT_MARKER_IDS.placeholder,
|
|
6891
|
+
re: /(^\s*\.\.\.\s*$)|(\[[^[\]\n<>]{2,120}\])|(<[^<>\n]{2,120}>)/
|
|
6892
|
+
},
|
|
6893
|
+
{
|
|
6894
|
+
id: ARTIFACT_MARKER_IDS.uncheckedTodo,
|
|
6895
|
+
re: /^\s*[-*]\s*\[ \]/
|
|
6896
|
+
},
|
|
6897
|
+
{
|
|
6898
|
+
id: ARTIFACT_MARKER_IDS.failedGate,
|
|
6899
|
+
re: /^\s*(Coverage|Approval):\s*FAIL\b/i
|
|
6900
|
+
}
|
|
6901
|
+
];
|
|
6902
|
+
/**
|
|
6903
|
+
* Which marker a single line carries, or null.
|
|
6904
|
+
*
|
|
6905
|
+
* Exported because the client prints the marker NAMES beside the quoted lines, and a second classifier that
|
|
6906
|
+
* re-derived them would be a second answer to the same question.
|
|
6907
|
+
*/
|
|
6908
|
+
function markerIdsOnLine(line) {
|
|
6909
|
+
const ids = [];
|
|
6910
|
+
for (const pattern of MARKER_PATTERNS) if (pattern.re.test(line)) ids.push(pattern.id);
|
|
6911
|
+
return ids;
|
|
6912
|
+
}
|
|
6913
|
+
/**
|
|
6914
|
+
* Classify one artifact's text.
|
|
6915
|
+
*
|
|
6916
|
+
* A verdict of `unfilled` means the document still carries the template's own placeholder text — the
|
|
6917
|
+
* evidence travels with it, line by line, so the refusal (and the client notice) can name what is missing
|
|
6918
|
+
* instead of asserting a state the reader cannot check.
|
|
6919
|
+
*/
|
|
6920
|
+
function classifyArtifact(text) {
|
|
6921
|
+
const lines = text.split(/\r?\n/);
|
|
6922
|
+
const hits = [];
|
|
6923
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
6924
|
+
const line = lines[i];
|
|
6925
|
+
for (const id of markerIdsOnLine(line)) hits.push({
|
|
6926
|
+
id,
|
|
6927
|
+
line: i + 1,
|
|
6928
|
+
text: line.trim()
|
|
6929
|
+
});
|
|
6930
|
+
}
|
|
6931
|
+
return {
|
|
6932
|
+
verdict: hits.some((hit) => hit.id === ARTIFACT_MARKER_IDS.placeholder) ? "unfilled" : "filled",
|
|
6933
|
+
hits
|
|
6934
|
+
};
|
|
6935
|
+
}
|
|
6936
|
+
/** The unfilled evidence only (what a refusal names). */
|
|
6937
|
+
function unfilledEvidence(result) {
|
|
6938
|
+
return result.hits.filter((hit) => hit.id === ARTIFACT_MARKER_IDS.placeholder);
|
|
6939
|
+
}
|
|
6940
|
+
/**
|
|
6941
|
+
* One line naming what is missing, for a refusal sentence.
|
|
6942
|
+
*
|
|
6943
|
+
* The line number and the text are both quoted: "line 12: <short title>" is a thing a reader can go and
|
|
6944
|
+
* look at, while "the requirements are not filled in" is an assertion they would have to take on trust.
|
|
6945
|
+
*/
|
|
6946
|
+
function describeEvidence(hits, limit = 3) {
|
|
6947
|
+
const shown = hits.slice(0, limit).map((hit) => "line " + String(hit.line) + ": " + hit.text);
|
|
6948
|
+
const rest = hits.length - shown.length;
|
|
6949
|
+
const suffix = rest > 0 ? " (and " + String(rest) + " more)" : "";
|
|
6950
|
+
return shown.join(" | ") + suffix;
|
|
6951
|
+
}
|
|
6952
|
+
//#endregion
|
|
6867
6953
|
//#region src/run-start.ts
|
|
6868
6954
|
/**
|
|
6869
6955
|
* PHASE 0 — STARTING A RUN IS A HUMAN DECISION, NOT A SIDE EFFECT OF SCAFFOLDING.
|
|
@@ -6986,6 +7072,43 @@ function readRunStartApproval(root, runId) {
|
|
|
6986
7072
|
* the same string instead of re-typing it (a re-typed reason is a caller that silently stops matching).
|
|
6987
7073
|
*/
|
|
6988
7074
|
const RUN_START_NOT_APPROVED = "run not started: phase 0 approval has not been granted";
|
|
7075
|
+
/**
|
|
7076
|
+
* PHASE 0 — THE GATE CANNOT BE RAISED BEFORE THERE IS A SPEC TO DECIDE ABOUT.
|
|
7077
|
+
*
|
|
7078
|
+
* THE DEFECT. `recursive_init` scaffolds Phase 0 as a TEMPLATE, and `recursive_ask gate=run-start` raised
|
|
7079
|
+
* "start this run or hold?" over it immediately — while every requirement was still `<short title>`, every
|
|
7080
|
+
* acceptance criterion was still `[observable condition 1]`, and nothing put the document in front of the
|
|
7081
|
+
* person at all. The owner: *"the card ui for accepting the spec appeared, but i was never shown the spec
|
|
7082
|
+
* before that so how could i approve if i havent seen it"*. Approving an unfilled template is not a decision
|
|
7083
|
+
* about a spec; there is no spec yet, and a card that asks the question anyway teaches a person to answer
|
|
7084
|
+
* without reading.
|
|
7085
|
+
*
|
|
7086
|
+
* ⚠ WHAT THIS DOES *NOT* TOUCH. It does not weaken the gate's own contract, it does not add a second way to
|
|
7087
|
+
* start a run, and it does not make the plugin the decider: it only refuses to ASK. A person's own answer
|
|
7088
|
+
* still wins (`recordRunStartAnswer` is unchanged), a spec still creates no goal, and cancellation / abort /
|
|
7089
|
+
* timeout are still unrelayable. The check runs BEFORE the question is put to anybody, so no card is shown
|
|
7090
|
+
* for a document that cannot be approved meaningfully.
|
|
7091
|
+
*
|
|
7092
|
+
* ⚠ AND IT IS A CHECK ON THE DOCUMENT, NOT ON THE CALLER. A `runId` that does not resolve is not this
|
|
7093
|
+
* refusal's business — the ask path already reports that — so the guard says `ok: true` there and lets the
|
|
7094
|
+
* existing route handle it.
|
|
7095
|
+
*/
|
|
7096
|
+
function runStartSpecGuard(root, runId) {
|
|
7097
|
+
const content = readRunStartArtifact(root, runId);
|
|
7098
|
+
if (content === null) return {
|
|
7099
|
+
ok: false,
|
|
7100
|
+
reason: toolError("RUN_START_SPEC_UNFILLED", "there is no Phase 0 document to approve: " + runStartArtifactPath(root, runId) + " does not exist yet")
|
|
7101
|
+
};
|
|
7102
|
+
const verdict = classifyArtifact(content);
|
|
7103
|
+
if (verdict.verdict === "filled") return { ok: true };
|
|
7104
|
+
const evidence = unfilledEvidence(verdict);
|
|
7105
|
+
const context = verdict.hits.filter((hit) => hit.id !== "placeholder");
|
|
7106
|
+
const contextNote = context.length === 0 ? "" : " (it also carries " + String(context.length) + " unfinished marker(s) of its own, starting at line " + String(context[0]?.line ?? 0) + ")";
|
|
7107
|
+
return {
|
|
7108
|
+
ok: false,
|
|
7109
|
+
reason: toolError("RUN_START_SPEC_UNFILLED", "00-requirements.md for run " + JSON.stringify(runId) + " still carries the template scaffold" + contextNote + ": " + describeEvidence(evidence))
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
6989
7112
|
//#endregion
|
|
6990
7113
|
//#region src/recursive_ask.tool.ts
|
|
6991
7114
|
/**
|
|
@@ -7201,7 +7324,7 @@ function pendingGateFor(artifactFile, artifactText) {
|
|
|
7201
7324
|
function createRecursiveAskTool(recursive) {
|
|
7202
7325
|
return defineTool({
|
|
7203
7326
|
name: "recursive_ask",
|
|
7204
|
-
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. For run-start the mounted human channel is asked first and its own selection wins; when that channel cannot deliver the question, the refusal names the cause and `relay=true` with an explicit `answer` records the person's relayed approval. One ask per step.",
|
|
7327
|
+
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. For run-start the mounted human channel is asked first and its own selection wins; when that channel cannot deliver the question, the refusal names the cause and `relay=true` with an explicit `answer` records the person's relayed approval. The run-start gate is REFUSED while the Phase 0 requirements document is still the unfilled template — the refusal quotes the placeholder lines, and there is nothing to approve until they are written. One ask per step.",
|
|
7205
7328
|
parameters: {
|
|
7206
7329
|
gate: {
|
|
7207
7330
|
type: "string",
|
|
@@ -7240,6 +7363,16 @@ function createRecursiveAskTool(recursive) {
|
|
|
7240
7363
|
const root = await recursive.resolveWorkspaceRoot(exec.agent);
|
|
7241
7364
|
if (!root) return { error: toolError("NO_WORKSPACE") };
|
|
7242
7365
|
const artifact = (isRunStartGate(gateId) ? RUN_START_ARTIFACT : args.artifact ?? GATE_DEFAULT_ARTIFACT[gateId]).trim();
|
|
7366
|
+
if (isRunStartGate(gateId)) {
|
|
7367
|
+
const guard = runStartSpecGuard(root, runId);
|
|
7368
|
+
if (!guard.ok) return {
|
|
7369
|
+
error: guard.reason,
|
|
7370
|
+
gate: RUN_START_GATE_ID,
|
|
7371
|
+
runId,
|
|
7372
|
+
artifact: RUN_START_ARTIFACT,
|
|
7373
|
+
question: buildAskQuestionFor(RUN_START_GATE_ID)
|
|
7374
|
+
};
|
|
7375
|
+
}
|
|
7243
7376
|
let question;
|
|
7244
7377
|
try {
|
|
7245
7378
|
question = buildAskQuestionFor(gateId);
|
|
@@ -8646,7 +8779,7 @@ async function delegateContinuable(input) {
|
|
|
8646
8779
|
const observed = await input.awaitRoundResult(childId, lastMessageId);
|
|
8647
8780
|
if (observed === null) return {
|
|
8648
8781
|
ok: false,
|
|
8649
|
-
reason: "no settlement has landed for round " + (round + 1) + " yet
|
|
8782
|
+
reason: "no settlement has landed for round " + (round + 1) + " yet, so the round is PARKED, not failed: the child may still be working. Resume it on a later turn with childId " + String(childId) + " (the `resumeChild` argument) — do not start a second child.",
|
|
8650
8783
|
childId,
|
|
8651
8784
|
messageIds,
|
|
8652
8785
|
rounds,
|
|
@@ -8860,6 +8993,21 @@ function validateReferences(root, references) {
|
|
|
8860
8993
|
checked
|
|
8861
8994
|
};
|
|
8862
8995
|
}
|
|
8996
|
+
/**
|
|
8997
|
+
* The status a record states, in ONE place, because the three states are the fix and a second writer would
|
|
8998
|
+
* drift from this one.
|
|
8999
|
+
*
|
|
9000
|
+
* The wording is chosen for two readers at once. The MODEL reads it to decide whether to resume or to give up
|
|
9001
|
+
* and obtain the result another way — the exact decision the live defect got wrong — so the token must not
|
|
9002
|
+
* read as a failure and must not need the rest of the document to be understood. A HUMAN reading the run tree
|
|
9003
|
+
* months later needs to tell "died" from "still working" at a glance. Hence `parked (still running; no
|
|
9004
|
+
* settlement yet)`: a third token rather than a renamed second, qualified with the two facts that separate it
|
|
9005
|
+
* from `failed`, and short enough to sit in a status line.
|
|
9006
|
+
*/
|
|
9007
|
+
function actionRecordStatus(input) {
|
|
9008
|
+
if (input.parked === true) return "parked (still running; no settlement yet)";
|
|
9009
|
+
return input.success ? "accepted" : "failed";
|
|
9010
|
+
}
|
|
8863
9011
|
function slugify(value) {
|
|
8864
9012
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
|
|
8865
9013
|
}
|
|
@@ -8875,7 +9023,10 @@ function slugify(value) {
|
|
|
8875
9023
|
* `Code Refs` strictly inside ## Inputs Provided. The linter resolves each of
|
|
8876
9024
|
* those through the heading body, so a field under another heading is not
|
|
8877
9025
|
* found at all.
|
|
8878
|
-
* A success:false attempt is written with a failed status and is NOT accepted.
|
|
9026
|
+
* A success:false attempt is written with a failed status and is NOT accepted. A round that PARKED is written
|
|
9027
|
+
* with a status of its own (`parked (still running; no settlement yet)`, see {@link actionRecordStatus}) and a
|
|
9028
|
+
* `Parked:` line instead of a `Failure:` one, because no settlement is not a death: it is the caller's signal to
|
|
9029
|
+
* resume the same child. Only a genuine failure carries `Failure:`.
|
|
8879
9030
|
*/
|
|
8880
9031
|
function writeActionRecord(input) {
|
|
8881
9032
|
const { root, runId } = input;
|
|
@@ -8913,8 +9064,8 @@ function writeActionRecord(input) {
|
|
|
8913
9064
|
"- Phase: " + input.phase,
|
|
8914
9065
|
"- Purpose: " + input.purpose,
|
|
8915
9066
|
"- Execution Mode: " + input.executionMode,
|
|
8916
|
-
"- Status: " + (input
|
|
8917
|
-
...input.success === false && input.failure !== void 0 ? ["- Failure: " + input.failure] : [],
|
|
9067
|
+
"- Status: " + actionRecordStatus(input),
|
|
9068
|
+
...input.success === false && input.failure !== void 0 ? [input.parked === true ? "- Parked: " + input.failure : "- Failure: " + input.failure] : [],
|
|
8918
9069
|
"- Stop Reason: " + (input.stopReason ?? "n/a"),
|
|
8919
9070
|
"- Timestamp: " + (/* @__PURE__ */ new Date()).toISOString(),
|
|
8920
9071
|
"",
|
|
@@ -10694,7 +10845,10 @@ var RecursiveRuntime = class extends Service {
|
|
|
10694
10845
|
} else if (continuable.ok && continuable.rounds.length > 0) {
|
|
10695
10846
|
result = continuable.rounds[continuable.rounds.length - 1].result ?? null;
|
|
10696
10847
|
if (!result) error = "continuable child produced no final result";
|
|
10697
|
-
} else
|
|
10848
|
+
} else {
|
|
10849
|
+
result = continuable.rounds[continuable.rounds.length - 1]?.result ?? null;
|
|
10850
|
+
if (result === null) error = continuable.reason ?? "continuable delegation failed";
|
|
10851
|
+
}
|
|
10698
10852
|
} else try {
|
|
10699
10853
|
result = await delegate({
|
|
10700
10854
|
subagents,
|
|
@@ -10719,7 +10873,7 @@ var RecursiveRuntime = class extends Service {
|
|
|
10719
10873
|
id: operation,
|
|
10720
10874
|
act: "delegate-review",
|
|
10721
10875
|
at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
10722
|
-
outcome: evaluation.accepted ? "accepted" : "unaccepted",
|
|
10876
|
+
outcome: parked ? "parked" : evaluation.accepted ? "accepted" : "unaccepted",
|
|
10723
10877
|
phase: input.phase
|
|
10724
10878
|
});
|
|
10725
10879
|
const actionRecordPath = writeActionRecord({
|
|
@@ -10738,7 +10892,8 @@ var RecursiveRuntime = class extends Service {
|
|
|
10738
10892
|
findings: evaluation.accepted && result?.structured ? [result.structured?.verdict ?? "accepted"] : void 0,
|
|
10739
10893
|
success: evaluation.accepted,
|
|
10740
10894
|
stopReason: result?.stopReason,
|
|
10741
|
-
|
|
10895
|
+
...parked ? { parked: true } : {},
|
|
10896
|
+
failure: evaluation.accepted ? void 0 : parked ? "no settlement had landed when the wait ended, so this round is PARKED, not failed: nothing was accepted and nothing was refused, and the child may still be working. The next step is to RESUME this round, not to re-dispatch it or replace the child: call `recursive_review` again on a later turn with childId " + String(continuable?.childId ?? input.childId) + " (the child the round was started for, which stays resumable). Diagnostics: tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") + ", names on offer [" + (this.lastProviderNames.join(", ") || "none") + "]; parent id " + (input.parent?.id ?? "none") + ", parent session keys [" + (input.parent === void 0 ? "no parent" : Object.keys(input.parent).join(", ")) + "]" : result == null ? input.mode !== "one-shot" ? "the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported, or never ran); tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") + ", names on offer [" + (this.lastProviderNames.join(", ") || "none") + "]; parent id " + (input.parent?.id ?? "none") + ", parent session keys [" + (input.parent === void 0 ? "no parent" : Object.keys(input.parent).join(", ")) + "]" : "no delegate result was produced by the one-shot path; tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") : "the delegation returned without acceptance; stop reason " + (result.stopReason ?? "none reported") + (result.success === false ? " (the child itself reported success:false)" : "")
|
|
10742
10897
|
});
|
|
10743
10898
|
const delegationMode = continuable !== null ? continuable.fellBackToOneShot ? "continuable-unavailable" : "continuable" : result !== null ? "one-shot" : "none";
|
|
10744
10899
|
return {
|
|
@@ -14660,4 +14815,4 @@ function apply(ctx, config) {
|
|
|
14660
14815
|
});
|
|
14661
14816
|
}
|
|
14662
14817
|
//#endregion
|
|
14663
|
-
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 };
|
|
14818
|
+
export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, actionRecordStatus, 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 };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IS THIS RUN SPEC STILL A HOLLOW TEMPLATE? — one answer, two callers.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. `recursive_init` scaffolds Phase 0 as a TEMPLATE: the requirement block
|
|
5
|
+
* still reads `### \`R1\` <short title>`, the acceptance criteria are still `[observable condition 1]`,
|
|
6
|
+
* the checklists are still unchecked and both gates still read `FAIL`. The owner's defect report is that
|
|
7
|
+
* `recursive_ask gate=run-start` raised the "start this run or hold?" gate while the document was in
|
|
8
|
+
* exactly that state — *"i was never shown the spec before that so how could i approve if i havent seen
|
|
9
|
+
* it"*. Approving an unfilled template is not a decision about a spec; there is no spec yet.
|
|
10
|
+
*
|
|
11
|
+
* So the QUESTION "is this document still the template?" must have ONE answer, and two consumers need it:
|
|
12
|
+
*
|
|
13
|
+
* 1. the SERVER gate (`recursive_ask`), which must REFUSE to raise the run-start question while the
|
|
14
|
+
* answer is yes, and
|
|
15
|
+
* 2. the CLIENT sheet (`client/spec-sheet.tsx`), which must SAY SO plainly rather than dress a hollow
|
|
16
|
+
* document up as an approvable one — the honesty rule of `client/settings-view.ts`, applied to a
|
|
17
|
+
* document instead of a value.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ THIS MODULE IS NODE-FREE ON PURPOSE. It is reached by both halves of the bundle, and the client bundle
|
|
20
|
+
* is a BROWSER closure: a `node:fs` import anywhere in its graph breaks the page. So the file reading stays
|
|
21
|
+
* in `run-start.ts` (which owns the artifact path) and this module takes TEXT and returns a VERDICT.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ AND IT PINS THE TEMPLATE, NOT A COPY OF IT. The markers below are the literal lines
|
|
24
|
+
* `init-templates.ts::requirementsContent` writes. A checker that instead carried its own copy of the whole
|
|
25
|
+
* template would silently stop matching the moment the template changed; a checker that carried a CHECKSUM
|
|
26
|
+
* would call every edited document filled and every untouched one unfilled on the strength of a byte count.
|
|
27
|
+
* Naming the placeholder markers is the check that keeps meaning what it says.
|
|
28
|
+
*/
|
|
29
|
+
/** What the checker decided about one artifact's text. */
|
|
30
|
+
export type ArtifactVerdict = 'unfilled' | 'filled';
|
|
31
|
+
/** One piece of evidence found in the document, quoted with its line number. */
|
|
32
|
+
export interface ArtifactMarkerHit {
|
|
33
|
+
/** Stable id of the marker that matched. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** 1-based line number in the document as it was read. */
|
|
36
|
+
line: number;
|
|
37
|
+
/** The line verbatim, trimmed of surrounding whitespace. */
|
|
38
|
+
text: string;
|
|
39
|
+
}
|
|
40
|
+
/** The verdict plus the evidence for it. */
|
|
41
|
+
export interface ArtifactVerdictResult {
|
|
42
|
+
verdict: ArtifactVerdict;
|
|
43
|
+
/** Marker hits, in line order. Empty exactly when the verdict is `filled`. */
|
|
44
|
+
hits: ArtifactMarkerHit[];
|
|
45
|
+
}
|
|
46
|
+
/** The named evidence classes, so a reader can tell a placeholder from an unmet gate. */
|
|
47
|
+
export declare const ARTIFACT_MARKER_IDS: {
|
|
48
|
+
readonly placeholder: "placeholder";
|
|
49
|
+
readonly uncheckedTodo: "unchecked-todo";
|
|
50
|
+
readonly failedGate: "failed-gate";
|
|
51
|
+
};
|
|
52
|
+
export type ArtifactMarkerId = (typeof ARTIFACT_MARKER_IDS)[keyof typeof ARTIFACT_MARKER_IDS];
|
|
53
|
+
/**
|
|
54
|
+
* Which marker a single line carries, or null.
|
|
55
|
+
*
|
|
56
|
+
* Exported because the client prints the marker NAMES beside the quoted lines, and a second classifier that
|
|
57
|
+
* re-derived them would be a second answer to the same question.
|
|
58
|
+
*/
|
|
59
|
+
export declare function markerIdsOnLine(line: string): ArtifactMarkerId[];
|
|
60
|
+
/**
|
|
61
|
+
* Classify one artifact's text.
|
|
62
|
+
*
|
|
63
|
+
* A verdict of `unfilled` means the document still carries the template's own placeholder text — the
|
|
64
|
+
* evidence travels with it, line by line, so the refusal (and the client notice) can name what is missing
|
|
65
|
+
* instead of asserting a state the reader cannot check.
|
|
66
|
+
*/
|
|
67
|
+
export declare function classifyArtifact(text: string): ArtifactVerdictResult;
|
|
68
|
+
/** The unfilled evidence only (what a refusal names). */
|
|
69
|
+
export declare function unfilledEvidence(result: ArtifactVerdictResult): ArtifactMarkerHit[];
|
|
70
|
+
/** The weak, contextual markers (unchecked boxes, FAIL gates). */
|
|
71
|
+
export declare function contextEvidence(result: ArtifactVerdictResult): ArtifactMarkerHit[];
|
|
72
|
+
/**
|
|
73
|
+
* One line naming what is missing, for a refusal sentence.
|
|
74
|
+
*
|
|
75
|
+
* The line number and the text are both quoted: "line 12: <short title>" is a thing a reader can go and
|
|
76
|
+
* look at, while "the requirements are not filled in" is an assertion they would have to take on trust.
|
|
77
|
+
*/
|
|
78
|
+
export declare function describeEvidence(hits: readonly ArtifactMarkerHit[], limit?: number): string;
|
package/lib/run-start.d.ts
CHANGED
|
@@ -53,3 +53,30 @@ export declare function readRunStartApproval(root: string, runId: string): {
|
|
|
53
53
|
* the same string instead of re-typing it (a re-typed reason is a caller that silently stops matching).
|
|
54
54
|
*/
|
|
55
55
|
export declare const RUN_START_NOT_APPROVED = "run not started: phase 0 approval has not been granted";
|
|
56
|
+
/**
|
|
57
|
+
* PHASE 0 — THE GATE CANNOT BE RAISED BEFORE THERE IS A SPEC TO DECIDE ABOUT.
|
|
58
|
+
*
|
|
59
|
+
* THE DEFECT. `recursive_init` scaffolds Phase 0 as a TEMPLATE, and `recursive_ask gate=run-start` raised
|
|
60
|
+
* "start this run or hold?" over it immediately — while every requirement was still `<short title>`, every
|
|
61
|
+
* acceptance criterion was still `[observable condition 1]`, and nothing put the document in front of the
|
|
62
|
+
* person at all. The owner: *"the card ui for accepting the spec appeared, but i was never shown the spec
|
|
63
|
+
* before that so how could i approve if i havent seen it"*. Approving an unfilled template is not a decision
|
|
64
|
+
* about a spec; there is no spec yet, and a card that asks the question anyway teaches a person to answer
|
|
65
|
+
* without reading.
|
|
66
|
+
*
|
|
67
|
+
* ⚠ WHAT THIS DOES *NOT* TOUCH. It does not weaken the gate's own contract, it does not add a second way to
|
|
68
|
+
* start a run, and it does not make the plugin the decider: it only refuses to ASK. A person's own answer
|
|
69
|
+
* still wins (`recordRunStartAnswer` is unchanged), a spec still creates no goal, and cancellation / abort /
|
|
70
|
+
* timeout are still unrelayable. The check runs BEFORE the question is put to anybody, so no card is shown
|
|
71
|
+
* for a document that cannot be approved meaningfully.
|
|
72
|
+
*
|
|
73
|
+
* ⚠ AND IT IS A CHECK ON THE DOCUMENT, NOT ON THE CALLER. A `runId` that does not resolve is not this
|
|
74
|
+
* refusal's business — the ask path already reports that — so the guard says `ok: true` there and lets the
|
|
75
|
+
* existing route handle it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function runStartSpecGuard(root: string, runId: string): {
|
|
78
|
+
ok: true;
|
|
79
|
+
} | {
|
|
80
|
+
ok: false;
|
|
81
|
+
reason: string;
|
|
82
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@try-works/dsh-recursive-mode",
|
|
3
3
|
"description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + 13 recursive_* tools (recursive_status, recursive_init, recursive_lock, recursive_lint, recursive_closeout, recursive_scratch, recursive_worktree, recursive_phase, recursive_audit_team, recursive_review, recursive_delegate, recursive_ask, recursive_preview)",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
package/src/client/contract.ts
CHANGED
|
@@ -62,6 +62,16 @@ export interface ClientSlots {
|
|
|
62
62
|
/** A registered slot's options. */
|
|
63
63
|
export interface SlotOptions {
|
|
64
64
|
name: string
|
|
65
|
+
/**
|
|
66
|
+
* The dispatch key of a KEYED seat (`tool.call.toolview` is one), required there and ignored elsewhere.
|
|
67
|
+
*
|
|
68
|
+
* ⚠ THIS FIELD WAS MISSING AND ITS ABSENCE WAS NOT COSMETIC: `slots.ts` registers the run-start spec sheet
|
|
69
|
+
* on `tool.call.toolview` under the tool's wire name, and the harness's own options type makes `key`
|
|
70
|
+
* mandatory for a keyed slot (`KindOptions` in `packages/client/ui-slots`, where a keyed registration with
|
|
71
|
+
* no key THROWS `keyed slot "<name>" requires options.key`). Without the field here the face did not match
|
|
72
|
+
* the API it describes, so `tsc --noEmit` rejected the registration and the sheet was unreachable code.
|
|
73
|
+
*/
|
|
74
|
+
key?: string
|
|
65
75
|
id?: string
|
|
66
76
|
order?: number
|
|
67
77
|
label?: string
|