dorfl 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advance.d.ts +8 -1
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +39 -6
- package/dist/advance.js.map +1 -1
- package/dist/apply-decide.d.ts +7 -1
- package/dist/apply-decide.d.ts.map +1 -1
- package/dist/apply-decide.js +23 -10
- package/dist/apply-decide.js.map +1 -1
- package/dist/apply-persist.d.ts +52 -8
- package/dist/apply-persist.d.ts.map +1 -1
- package/dist/apply-persist.js +78 -9
- package/dist/apply-persist.js.map +1 -1
- package/dist/frontmatter.d.ts +17 -10
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +3 -3
- package/dist/frontmatter.js.map +1 -1
- package/dist/protocol/WORK-CONTRACT.md +2 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +2 -1
- package/dist/skills/triage-observations/SKILL.md +1 -1
- package/dist/triage-gate.d.ts +4 -2
- package/dist/triage-gate.d.ts.map +1 -1
- package/dist/triage-gate.js.map +1 -1
- package/dist/triage-persist.d.ts +55 -5
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +68 -7
- package/dist/triage-persist.js.map +1 -1
- package/package.json +1 -1
- package/src/advance.ts +52 -4
- package/src/apply-decide.ts +23 -10
- package/src/apply-persist.ts +86 -9
- package/src/frontmatter.ts +17 -10
- package/src/triage-gate.ts +4 -2
- package/src/triage-persist.ts +110 -9
package/src/advance.ts
CHANGED
|
@@ -32,10 +32,13 @@ import type {ObservationTriage} from './config.js';
|
|
|
32
32
|
import {
|
|
33
33
|
autoDispositionObservation,
|
|
34
34
|
promoteObservation,
|
|
35
|
+
stampTriagedMarker,
|
|
35
36
|
type AutoDispositionOptions,
|
|
36
37
|
type AutoDispositionResult,
|
|
37
38
|
type PromoteObservationOptions,
|
|
38
39
|
type PromoteObservationResult,
|
|
40
|
+
type StampTriagedMarkerOptions,
|
|
41
|
+
type StampTriagedMarkerResult,
|
|
39
42
|
} from './triage-persist.js';
|
|
40
43
|
import {mintAdr, type MintAdrOptions, type MintAdrResult} from './mint-adr.js';
|
|
41
44
|
import {LIFECYCLE_CAS_CONTENTION} from './advancing-lock.js';
|
|
@@ -57,6 +60,8 @@ import {
|
|
|
57
60
|
} from './surface-persist.js';
|
|
58
61
|
import {
|
|
59
62
|
applyAnsweredQuestions,
|
|
63
|
+
hasAppliedAnswersRecord,
|
|
64
|
+
TRIAGED_RESOLVE,
|
|
60
65
|
type ApplyAnsweredQuestionsOptions,
|
|
61
66
|
type ApplyAnsweredQuestionsResult,
|
|
62
67
|
} from './apply-persist.js';
|
|
@@ -324,6 +329,15 @@ export interface AdvanceContext {
|
|
|
324
329
|
* commit). Tests inject a spy; production uses {@link autoDispositionObservation}.
|
|
325
330
|
*/
|
|
326
331
|
autoDisposition?: (options: AutoDispositionOptions) => AutoDispositionResult;
|
|
332
|
+
/**
|
|
333
|
+
* Back-stamp the `triaged:` settled marker on a note that was ALREADY
|
|
334
|
+
* resolved-and-kept before the apply rung learned to stamp it (the legacy arm of
|
|
335
|
+
* the triage rung's settled guard). Tests inject a spy; production uses
|
|
336
|
+
* {@link stampTriagedMarker}.
|
|
337
|
+
*/
|
|
338
|
+
stampTriaged?: (
|
|
339
|
+
options: StampTriagedMarkerOptions,
|
|
340
|
+
) => StampTriagedMarkerResult;
|
|
327
341
|
/**
|
|
328
342
|
* Promote an answered observation: CAS-create a new backlog stub keyed on the
|
|
329
343
|
* NEW item's identity, then record + resolve the observation. Tests inject a
|
|
@@ -910,10 +924,20 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
|
|
|
910
924
|
// note. (Replaces the old `detectObservationLimbo` special-case: there is no
|
|
911
925
|
// limbo any more — an UNtriaged observation always surfaces the deterministic
|
|
912
926
|
// question below; a SETTLED one is a calm no-op here.)
|
|
927
|
+
//
|
|
928
|
+
// SECOND ARM — the LEGACY back-fill (ADR
|
|
929
|
+
// `resolve-settles-the-question-loop-not-the-note`): a note the apply rung
|
|
930
|
+
// resolved-and-KEPT before it learned to stamp the marker carries the
|
|
931
|
+
// ENGINE-WRITTEN `## Applied answers` record but NO marker, which is the exact
|
|
932
|
+
// shape this rung would re-ask forever. That record is PROOF the engine already
|
|
933
|
+
// applied a human's answers to this note, so we stamp what the apply rung would
|
|
934
|
+
// have written and no-op. Self-healing and one-shot: the stamped note drops out
|
|
935
|
+
// of the pool, so this arm can never fire twice for the same note.
|
|
913
936
|
{
|
|
914
937
|
const itemRel = findItemPath(cwd, input.namespace, input.slug);
|
|
915
938
|
if (itemRel !== undefined) {
|
|
916
|
-
const
|
|
939
|
+
const body = readFileSync(join(cwd, itemRel), 'utf8');
|
|
940
|
+
const fm = parseFrontmatter(body);
|
|
917
941
|
if (fm.triaged !== undefined && fm.triaged !== '') {
|
|
918
942
|
return {
|
|
919
943
|
exitCode: 0,
|
|
@@ -921,6 +945,21 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
|
|
|
921
945
|
message: `triage ${item}: already triaged (triaged:${fm.triaged}) — nothing to do.`,
|
|
922
946
|
};
|
|
923
947
|
}
|
|
948
|
+
if (hasAppliedAnswersRecord(body)) {
|
|
949
|
+
const stamp = context.stampTriaged ?? stampTriagedMarker;
|
|
950
|
+
const result = stamp({
|
|
951
|
+
cwd,
|
|
952
|
+
item,
|
|
953
|
+
itemPath: itemRel,
|
|
954
|
+
value: TRIAGED_RESOLVE,
|
|
955
|
+
reason:
|
|
956
|
+
'Back-filled: the note carries an engine-written applied-answers ' +
|
|
957
|
+
'record, so its triage question-loop was already settled by a human ' +
|
|
958
|
+
'answer before the apply rung stamped the marker.',
|
|
959
|
+
note,
|
|
960
|
+
});
|
|
961
|
+
return {exitCode: 0, outcome: 'no-op', message: result.message};
|
|
962
|
+
}
|
|
924
963
|
}
|
|
925
964
|
}
|
|
926
965
|
|
|
@@ -951,9 +990,10 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
|
|
|
951
990
|
if (decision.auto === true) {
|
|
952
991
|
// A no-question case (duplicate / map) — auto-disposition WITHOUT a
|
|
953
992
|
// question. BOTH discharge the redundant note BY DELETION (a duplicate is a
|
|
954
|
-
// redundant copy; a map is already covered by the item it maps onto)
|
|
955
|
-
//
|
|
956
|
-
//
|
|
993
|
+
// redundant copy; a map is already covered by the item it maps onto), so an
|
|
994
|
+
// AUTO disposition never RESTS a note — resting a KEPT note is the
|
|
995
|
+
// human-answered `resolve` verdict's job on the apply rung. NEVER auto-deletes
|
|
996
|
+
// a NON-redundant signal; NEVER auto-promotes.
|
|
957
997
|
const dispose = context.autoDisposition ?? autoDispositionObservation;
|
|
958
998
|
const result = dispose({
|
|
959
999
|
cwd,
|
|
@@ -1654,6 +1694,14 @@ async function applyAgenticDecision(
|
|
|
1654
1694
|
// the sibling of `dispose`, which git-rm's it or moves it to a terminal). `resolveReason` is advisory
|
|
1655
1695
|
// context only; the durable disposition record is the harvested `## Applied
|
|
1656
1696
|
// answers` block the resolve-fully path writes (NOT a separate convention).
|
|
1697
|
+
//
|
|
1698
|
+
// Because the note is KEPT, that same commit also stamps `triaged: resolve` on
|
|
1699
|
+
// it (ADR `resolve-settles-the-question-loop-not-the-note`): a kept note stays
|
|
1700
|
+
// in the inbox the triage rung scans, and without the marker its two
|
|
1701
|
+
// classifier signals (`needsAnswers:false`, no sidecar) are IDENTICAL to a note
|
|
1702
|
+
// that was never triaged — so the next tick re-surfaced the same engine-built
|
|
1703
|
+
// triage question forever. The stamp is done by the persist, not here, so no
|
|
1704
|
+
// caller of the resolve-fully path can forget it.
|
|
1657
1705
|
const apply = context.applyPersist ?? applyAnsweredQuestions;
|
|
1658
1706
|
try {
|
|
1659
1707
|
const result = apply({cwd, item, itemPath, note});
|
package/src/apply-decide.ts
CHANGED
|
@@ -40,7 +40,13 @@ import {
|
|
|
40
40
|
* by the task `apply-decide-resolve-verdict-mint-nothing` so the decider can
|
|
41
41
|
* honestly handle "the human answered, keep the note on record, mint nothing"
|
|
42
42
|
* (previously it had no valid verdict for that case and looped on `ask`,
|
|
43
|
-
* re-surfacing an already-answered question every tick).
|
|
43
|
+
* re-surfacing an already-answered question every tick). Its MEANING was SHARPENED
|
|
44
|
+
* by ADR `resolve-settles-the-question-loop-not-the-note`: `resolve` settles the
|
|
45
|
+
* QUESTION-LOOP on a note that REMAINS a live signal (and the persist stamps
|
|
46
|
+
* `triaged:` so the triage rung stops re-asking it, closing a second re-ask loop
|
|
47
|
+
* the first fix left open); a note whose SIGNAL is finished is `dispose`, not
|
|
48
|
+
* `resolve` — a dead note kept and stamped "resolved" is exactly the
|
|
49
|
+
* backward-artifact-in-a-forward-bucket the work contract forbids. `adr` was DEFERRED at the
|
|
44
50
|
* keystone launch (no
|
|
45
51
|
* ADR-mint path existed yet) and is now WIRED by the follow-on task
|
|
46
52
|
* `agentic-apply-mint-adr-route`, which added the {@link
|
|
@@ -201,15 +207,22 @@ export function buildApplyDecisionPrompt(input: ApplyDecisionInput): string {
|
|
|
201
207
|
` \`reason:\` frontmatter); a SPEC is git-mv-ed to \`specs/dropped/\` (RETAINED).`,
|
|
202
208
|
` A task can NEVER be hard-deleted by the apply rung \u2014 dispose is the`,
|
|
203
209
|
` only path off the board.`,
|
|
204
|
-
` - "resolve": the answer SETTLES
|
|
205
|
-
` task/spec/adr)
|
|
206
|
-
`
|
|
207
|
-
` answer
|
|
208
|
-
` harvested into the item body
|
|
209
|
-
`
|
|
210
|
-
`
|
|
211
|
-
`
|
|
212
|
-
`
|
|
210
|
+
` - "resolve": the answer SETTLES the QUESTION-LOOP and there is NOTHING to`,
|
|
211
|
+
` mint (no task/spec/adr), AND the note is STILL A LIVE SIGNAL worth keeping`,
|
|
212
|
+
` in the inbox \u2014 e.g. a standing map of a known gap, or accepted residue the`,
|
|
213
|
+
` answer explicitly says to keep on record until it is acted on. The answers`,
|
|
214
|
+
` are harvested into the item body, the loop is cleared, and the note is`,
|
|
215
|
+
` RETAINED and STAMPED as triaged, so it is never re-asked this question.`,
|
|
216
|
+
` Emit {"outcome":"resolve","resolveReason":"\u2026"}.`,
|
|
217
|
+
` PICK BETWEEN "resolve" AND "dispose" ON LIVENESS, NOT ON POLITENESS: both`,
|
|
218
|
+
` close the loop and mint nothing, and the ONLY question is whether the note`,
|
|
219
|
+
` is still a live signal AFTER this answer. If the answer means the signal is`,
|
|
220
|
+
` FINISHED (the thing was fixed, it was already covered elsewhere, it is`,
|
|
221
|
+
` obsolete, or its whole content is now carried by a task/spec/ADR/commit),`,
|
|
222
|
+
` that note stops being live \u2014 emit "dispose", NOT "resolve". "resolve" is`,
|
|
223
|
+
` only correct when the note still carries a signal nothing else records.`,
|
|
224
|
+
` Do NOT use "resolve" as a soft "dispose": a note kept only to show it was`,
|
|
225
|
+
` handled is a backward artifact in a forward bucket.`,
|
|
213
226
|
` - "ask": you need more from the human before acting. Emit`,
|
|
214
227
|
` {"outcome":"ask","question":"\u2026"} \u2014 ask everything you still need as ONE`,
|
|
215
228
|
` batch (never a drip); the engine appends it and re-pauses.`,
|
package/src/apply-persist.ts
CHANGED
|
@@ -42,17 +42,26 @@ import {workItemRel} from './work-layout.js';
|
|
|
42
42
|
* hard-deleted from here — dispose is the only path off the board; OR
|
|
43
43
|
* - **resolve fully** (the default) — clear `needsAnswers` + DELETE the sidecar
|
|
44
44
|
* in the SAME atomic commit (the invariant `needsAnswers:false ⟺ no active
|
|
45
|
-
* sidecar`); the item advances toward build by its normal lifecycle.
|
|
45
|
+
* sidecar`); the item advances toward build by its normal lifecycle. For an
|
|
46
|
+
* OBSERVATION this ALSO stamps the `triaged:` settled marker (see
|
|
47
|
+
* {@link TRIAGED_RESOLVE}) in that same commit — the note is KEPT, so it needs
|
|
48
|
+
* a durable record that its triage question-loop is CLOSED or the triage rung
|
|
49
|
+
* re-asks it forever (ADR `resolve-settles-the-question-loop-not-the-note`).
|
|
46
50
|
*
|
|
47
51
|
* **The disposition vocabulary is GONE** (task
|
|
48
52
|
* `agentic-apply-retire-disposition-vocabulary`): the apply rung no longer reads a
|
|
49
|
-
* per-entry `disposition=` token
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* per-entry `disposition=` token and no longer runs a most-decisive picker. A
|
|
54
|
+
* sidecar entry is BINARY (no-answer | answered); what to DO with a fully-answered
|
|
55
|
+
* OBSERVATION is decided by the AGENTIC apply decision in the advance tick
|
|
56
|
+
* (`advance.ts` `applyRung` over the shared `decide` engine), which then routes
|
|
57
|
+
* here (re-pause / dispose / resolve / via `promoteObservation` for a mint).
|
|
58
|
+
*
|
|
59
|
+
* What the retirement removed was the human-stamped `disposition=`/`promote-*`
|
|
60
|
+
* TOKEN vocabulary, NOT the settled AXIS: a note that the `resolve` verdict KEEPS
|
|
61
|
+
* still rests as `triaged: resolve` + `needsAnswers:false` + no sidecar. That is
|
|
62
|
+
* the one resting state a note has, and it says "the question-loop is settled",
|
|
63
|
+
* NOT "the signal is finished" (a finished signal is `dispose`d — it leaves the
|
|
64
|
+
* inbox by deletion).
|
|
56
65
|
*
|
|
57
66
|
* The work-item (task/spec) terminal MOVES (`tasks/cancelled`, `specs/dropped`) and
|
|
58
67
|
* the stuck (lock `state: stuck`) LIFECYCLE state are a SEPARATE lifecycle concern (a
|
|
@@ -87,6 +96,32 @@ export {APPLY_LIFECYCLE_FOLDERS, resolveItemPathByIdentity};
|
|
|
87
96
|
/** Marker that opens the applied-answers record in an item body. */
|
|
88
97
|
const APPLIED_HEADING = '## Applied answers';
|
|
89
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The `triaged:` settled-marker VALUE the resolve-fully path stamps on a KEPT
|
|
101
|
+
* OBSERVATION (ADR `resolve-settles-the-question-loop-not-the-note`).
|
|
102
|
+
*
|
|
103
|
+
* The `resolve` verdict is the ONE apply terminal that RETAINS an observation in
|
|
104
|
+
* the inbox. Every other terminal removes it from the triage pool by MOVING or
|
|
105
|
+
* DELETING the file (mint → `git rm` + the new artifact; dispose → `git rm` /
|
|
106
|
+
* terminal folder), so only this one needs an in-file record that the note has
|
|
107
|
+
* been through triage. Without it the resolved note is byte-indistinguishable, to
|
|
108
|
+
* the classifier's two signals (`needsAnswers` + sidecar), from a note that was
|
|
109
|
+
* NEVER triaged: `needsAnswers:false` + no sidecar ⇒ ANALYSE ⇒ `triage-observation`
|
|
110
|
+
* ⇒ the engine-built triage question is surfaced AGAIN, and the human is asked
|
|
111
|
+
* something they already answered (observed live in the `rocketh` consumer repo:
|
|
112
|
+
* surface → answer → resolve → the byte-identical question re-surfaced).
|
|
113
|
+
*
|
|
114
|
+
* The marker is the axis the READ side already models end-to-end — `frontmatter.ts`
|
|
115
|
+
* parses it, `ledger-read.ts` carries it, `lifecycle-pools.ts` drops a marked
|
|
116
|
+
* observation out of the CREATE-side triage pool, and `advance.ts`'s triage rung
|
|
117
|
+
* no-ops on it for an explicit `obs:<slug>`. Only the WRITER was missing.
|
|
118
|
+
*
|
|
119
|
+
* It does NOT strand a later answer: an ANSWERED sidecar dominates the marker (ADR
|
|
120
|
+
* `answered-observation-sidecar-dominates-triaged-marker`), so a genuinely NEW
|
|
121
|
+
* question about a settled note still routes to apply and is still answerable.
|
|
122
|
+
*/
|
|
123
|
+
export const TRIAGED_RESOLVE = 'resolve';
|
|
124
|
+
|
|
90
125
|
/**
|
|
91
126
|
* The STRUCTURAL fence the templates wrap the transient open-questions block in
|
|
92
127
|
* — an HTML-comment marker pair, mirroring the existing `<!-- dorfl-…
|
|
@@ -108,6 +143,21 @@ const APPLIED_HEADING = '## Applied answers';
|
|
|
108
143
|
export const OPEN_QUESTIONS_MARKER_OPEN = '<!-- open-questions -->';
|
|
109
144
|
export const OPEN_QUESTIONS_MARKER_CLOSE = '<!-- /open-questions -->';
|
|
110
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Does this item body carry the ENGINE-WRITTEN applied-answers record?
|
|
148
|
+
*
|
|
149
|
+
* Only {@link applyAnsweredQuestions} writes this heading, and only after a human
|
|
150
|
+
* answered EVERY open question on the item, so its presence is PROOF the engine
|
|
151
|
+
* has applied a human's answers here — not a prose heuristic. The triage rung uses
|
|
152
|
+
* it as the legacy back-fill trigger for notes resolved-and-kept before the
|
|
153
|
+
* `triaged:` stamp existed (ADR `resolve-settles-the-question-loop-not-the-note`).
|
|
154
|
+
*/
|
|
155
|
+
export function hasAppliedAnswersRecord(body: string): boolean {
|
|
156
|
+
return new RegExp(`^${APPLIED_HEADING}\\b`, 'm').test(
|
|
157
|
+
body.replace(/\r\n/g, '\n'),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
export interface ApplyAnsweredQuestionsOptions {
|
|
112
162
|
/** Working clone/worktree the apply commits in. */
|
|
113
163
|
cwd: string;
|
|
@@ -490,7 +540,10 @@ export function applyAnsweredQuestions(
|
|
|
490
540
|
// compatible — no marker ⇒ identical bytes. The re-pause path above is
|
|
491
541
|
// deliberately untouched (D3): its open-questions block is still open.
|
|
492
542
|
const reconciledBody = stripOpenQuestionsBlocks(baseBody);
|
|
493
|
-
const resolvedBody =
|
|
543
|
+
const resolvedBody = withTriagedMarkerIfKeptNote(
|
|
544
|
+
withAppliedAnswers(reconciledBody, model.entries),
|
|
545
|
+
item,
|
|
546
|
+
);
|
|
494
547
|
const result = applyAtomic({
|
|
495
548
|
cwd,
|
|
496
549
|
itemPath,
|
|
@@ -512,6 +565,30 @@ export function applyAnsweredQuestions(
|
|
|
512
565
|
};
|
|
513
566
|
}
|
|
514
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Stamp the `triaged:` settled marker on a resolve-fully body — but ONLY for an
|
|
570
|
+
* OBSERVATION (the one item type this path KEEPS in a re-scanned inbox).
|
|
571
|
+
*
|
|
572
|
+
* Structural, not caller-supplied, so it cannot be forgotten by a future caller:
|
|
573
|
+
* every route that reaches the resolve-fully branch with an observation identity
|
|
574
|
+
* has, by definition, just closed that note's question-loop while retaining the
|
|
575
|
+
* file. A TASK/SPEC is deliberately EXCLUDED — it rests in a lifecycle FOLDER (its
|
|
576
|
+
* status), is not enumerated from the observation inbox, and has no triage rung to
|
|
577
|
+
* re-ask it; stamping a triage marker on one would invent a second status axis for
|
|
578
|
+
* an item whose status is already the folder.
|
|
579
|
+
*
|
|
580
|
+
* Idempotent ({@link setFrontmatterMarker} replaces an existing key) and
|
|
581
|
+
* fence-creating (observations are routinely born fence-less), so re-resolving a
|
|
582
|
+
* note after a genuinely new question re-stamps rather than duplicating.
|
|
583
|
+
*/
|
|
584
|
+
function withTriagedMarkerIfKeptNote(body: string, item: string): string {
|
|
585
|
+
const {type} = resolveSidecarIdentity(item);
|
|
586
|
+
if (type !== 'observation') {
|
|
587
|
+
return body;
|
|
588
|
+
}
|
|
589
|
+
return setFrontmatterMarker(body, 'triaged', TRIAGED_RESOLVE);
|
|
590
|
+
}
|
|
591
|
+
|
|
515
592
|
interface DisposeInput {
|
|
516
593
|
cwd: string;
|
|
517
594
|
item: string;
|
package/src/frontmatter.ts
CHANGED
|
@@ -94,13 +94,20 @@ export interface Frontmatter {
|
|
|
94
94
|
*/
|
|
95
95
|
needsAnswers: boolean | undefined;
|
|
96
96
|
/**
|
|
97
|
-
* The triage SETTLED marker (US #30). A non-empty `triaged:` value
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
97
|
+
* The triage SETTLED marker (US #30). A non-empty `triaged:` value means this
|
|
98
|
+
* observation's triage QUESTION-LOOP has been settled by a human's answer, so it
|
|
99
|
+
* DROPS OUT of the triage candidate pool and is never re-asked. `undefined` when
|
|
100
|
+
* omitted (an UNTRIAGED observation, still in the pool). Carried so the
|
|
101
|
+
* lifecycle-pool enumeration (`advance-autopick-lifecycle-pools`) can exclude
|
|
102
|
+
* settled observations from the triage selection.
|
|
103
|
+
*
|
|
104
|
+
* The one WRITER today is the apply rung's `resolve` verdict, which stamps
|
|
105
|
+
* `triaged: resolve` on a note it settles and KEEPS (ADR
|
|
106
|
+
* `resolve-settles-the-question-loop-not-the-note`). It says the question-loop is
|
|
107
|
+
* settled, NOT that the signal is finished — a finished signal has no resting
|
|
108
|
+
* state at all (it is deleted), and an ANSWERED sidecar still dominates this
|
|
109
|
+
* marker (ADR `answered-observation-sidecar-dominates-triaged-marker`), so a
|
|
110
|
+
* genuinely new question about a settled note is still asked and answered.
|
|
104
111
|
*/
|
|
105
112
|
triaged: string | undefined;
|
|
106
113
|
/** Slugs this item is blocked by; `[]` when omitted or empty. */
|
|
@@ -241,9 +248,9 @@ export function setNeedsAnswersMarker(content: string, value: boolean): string {
|
|
|
241
248
|
/**
|
|
242
249
|
* Set (or replace) a top-level scalar `<key>: <value>` frontmatter marker on a
|
|
243
250
|
* `work/` markdown document, returning the new content. The generalised form of
|
|
244
|
-
* {@link setNeedsAnswersMarker} the advance APPLY rung uses to stamp
|
|
245
|
-
* `triaged:
|
|
246
|
-
* settled observation drops out of the candidate pool and is never re-asked. If a
|
|
251
|
+
* {@link setNeedsAnswersMarker} the advance APPLY rung uses to stamp the
|
|
252
|
+
* `triaged: resolve` marker (US #30) on a note a human's answer settles but KEEPS
|
|
253
|
+
* — so a settled observation drops out of the candidate pool and is never re-asked. If a
|
|
247
254
|
* `<key>:` line already exists it is REPLACED (idempotent); otherwise it is
|
|
248
255
|
* appended as the last line inside the frontmatter fence.
|
|
249
256
|
*
|
package/src/triage-gate.ts
CHANGED
|
@@ -26,8 +26,10 @@ import {extractJsonObjectSpan} from './verdict-json.js';
|
|
|
26
26
|
* - `map` — an UNAMBIGUOUS map onto an existing item → DISCHARGE the redundant
|
|
27
27
|
* note BY DELETION too (it is already covered by the item it maps onto, so it
|
|
28
28
|
* carries no unique signal; the mapping is recorded in the commit message).
|
|
29
|
-
*
|
|
30
|
-
* `agentic-apply-retire-disposition-vocabulary`)
|
|
29
|
+
* An AUTO disposition never RESTS a note (task
|
|
30
|
+
* `agentic-apply-retire-disposition-vocabulary`); the one path that KEEPS a
|
|
31
|
+
* note is the human-answered `resolve` verdict on the apply rung (ADR
|
|
32
|
+
* `resolve-settles-the-question-loop-not-the-note`).
|
|
31
33
|
*
|
|
32
34
|
* It is the DIRECT mirror of `surface-gate.ts`'s spawn→emit→parse seam, with a
|
|
33
35
|
* narrower emitted payload (`{auto, …}` rather than `{questions}`). Production
|
package/src/triage-persist.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {readFileSync} from 'node:fs';
|
|
1
|
+
import {readFileSync, writeFileSync} from 'node:fs';
|
|
2
2
|
import {join} from 'node:path';
|
|
3
3
|
import {run, type RunResult} from './git.js';
|
|
4
4
|
import {workItemRel} from './work-layout.js';
|
|
@@ -25,8 +25,12 @@ import {setFrontmatterMarker} from './frontmatter.js';
|
|
|
25
25
|
* 1. **the conservative auto-disposition** ({@link autoDispositionObservation},
|
|
26
26
|
* US #17, `observationTriage: 'auto'`-gated): act on the no-question case on
|
|
27
27
|
* the UNTRIAGED observation in ONE local commit. BOTH no-question cases now
|
|
28
|
-
* DISCHARGE the redundant note BY DELETION
|
|
29
|
-
*
|
|
28
|
+
* DISCHARGE the redundant note BY DELETION — an AUTO disposition never RESTS a
|
|
29
|
+
* note, because both of its cases (duplicate / map) are by definition notes
|
|
30
|
+
* that carry no unique signal (task `agentic-apply-retire-disposition-
|
|
31
|
+
* vocabulary`; the ONE case that does rest a note is the human-answered
|
|
32
|
+
* `resolve` verdict on the APPLY rung, see ADR `resolve-settles-the-question-
|
|
33
|
+
* loop-not-the-note`):
|
|
30
34
|
* - `duplicate` → `git rm` the duplicate in a standalone commit, the
|
|
31
35
|
* duplicated-of identity + reason in the commit message (git history =
|
|
32
36
|
* archive). A duplicate is a redundant copy of an already-captured signal,
|
|
@@ -34,7 +38,6 @@ import {setFrontmatterMarker} from './frontmatter.js';
|
|
|
34
38
|
* - `map` → the note is already covered by the existing item it maps onto, so
|
|
35
39
|
* it is settled — `git rm` it in a standalone commit, the mapped-onto
|
|
36
40
|
* identity + reason recorded in the commit message (mirroring `duplicate`).
|
|
37
|
-
* There is no resting `triaged:keep` note any more.
|
|
38
41
|
* 2. **promote → SELF-CONTAINED new-item creation + DELETE through the CAS**
|
|
39
42
|
* ({@link promoteObservation}, US #1/#3/#8): an ANSWERED "promote" drafts a
|
|
40
43
|
* new `work/tasks/ready/<new-slug>.md` whose body is built FROM the
|
|
@@ -120,6 +123,102 @@ export class TriagePersistError extends Error {
|
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
// --- Back-stamping a KEPT note that was resolved before the marker existed ---
|
|
127
|
+
|
|
128
|
+
export interface StampTriagedMarkerOptions {
|
|
129
|
+
/** Working clone/worktree the stamp commits in. */
|
|
130
|
+
cwd: string;
|
|
131
|
+
/** The namespaced observation identity (`observation:<slug>`). */
|
|
132
|
+
item: string;
|
|
133
|
+
/** The observation file path RELATIVE to `cwd`. */
|
|
134
|
+
itemPath: string;
|
|
135
|
+
/** The `triaged:` value to stamp (the apply rung's settled disposition). */
|
|
136
|
+
value: string;
|
|
137
|
+
/** Why the stamp is being back-filled (rides the commit message). */
|
|
138
|
+
reason: string;
|
|
139
|
+
/** Advisory committer id for the commit subject. Defaults to git user.name. */
|
|
140
|
+
by?: string;
|
|
141
|
+
/** Environment for child git processes. */
|
|
142
|
+
env?: NodeJS.ProcessEnv;
|
|
143
|
+
/** Sink for human-readable progress notes. */
|
|
144
|
+
note?: (message: string) => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface StampTriagedMarkerResult {
|
|
148
|
+
/** The commit sha the stamp produced, or `undefined` when the body was already marked. */
|
|
149
|
+
commit?: string;
|
|
150
|
+
/** A human-readable summary. */
|
|
151
|
+
message: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Stamp the `triaged:` settled marker onto an observation that was ALREADY
|
|
156
|
+
* resolved-and-kept, in ONE local commit — the SELF-HEALING back-fill for notes
|
|
157
|
+
* resolved BEFORE the apply rung learned to stamp it (ADR
|
|
158
|
+
* `resolve-settles-the-question-loop-not-the-note`).
|
|
159
|
+
*
|
|
160
|
+
* Why a back-fill exists at all: the fix that makes `resolve` stamp the marker is
|
|
161
|
+
* forward-only, so every note a repo resolved under the old code still rests
|
|
162
|
+
* `needsAnswers:false` + no sidecar + NO marker — the exact shape the triage rung
|
|
163
|
+
* re-asks. Those notes are not hypothetical (a single `rocketh` cycle re-asked
|
|
164
|
+
* four, with eleven more queued behind them), and they are un-fixable from the
|
|
165
|
+
* human's side: answering again just resolves again into the same shape.
|
|
166
|
+
*
|
|
167
|
+
* The trigger is PROOF, not a heuristic: the caller fires this only when the body
|
|
168
|
+
* carries the ENGINE-WRITTEN `## Applied answers` record, which only
|
|
169
|
+
* `apply-persist.ts` writes and only after a human answered every open question.
|
|
170
|
+
* A hand-written note cannot accidentally acquire it, and the stamp is exactly
|
|
171
|
+
* what the apply rung WOULD have written had it known how.
|
|
172
|
+
*
|
|
173
|
+
* ONE commit touching ONE file, so the tree-less publish (`pushTreelessResult`)
|
|
174
|
+
* carries it to the arbiter like any other triage-rung marker write; once landed
|
|
175
|
+
* the note drops out of the triage pool and this path is never taken again.
|
|
176
|
+
*/
|
|
177
|
+
export function stampTriagedMarker(
|
|
178
|
+
options: StampTriagedMarkerOptions,
|
|
179
|
+
): StampTriagedMarkerResult {
|
|
180
|
+
const {cwd, item, itemPath, value, reason, env} = options;
|
|
181
|
+
const note = options.note ?? (() => {});
|
|
182
|
+
|
|
183
|
+
if (gitSoft(['rev-parse', '--git-dir'], cwd, env).status !== 0) {
|
|
184
|
+
throw new TriagePersistError('not inside a git repository');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const abs = join(cwd, itemPath);
|
|
188
|
+
const before = readFileSync(abs, 'utf8');
|
|
189
|
+
const after = setFrontmatterMarker(before, 'triaged', value);
|
|
190
|
+
if (after === before) {
|
|
191
|
+
// Already marked (or a malformed fence `setFrontmatterMarker` refuses to
|
|
192
|
+
// rewrite) — nothing to commit; never produce an empty commit.
|
|
193
|
+
const message = `triage ${item}: already carries triaged:${value} — no stamp needed.`;
|
|
194
|
+
note(message);
|
|
195
|
+
return {message};
|
|
196
|
+
}
|
|
197
|
+
writeFileSync(abs, after);
|
|
198
|
+
const by = options.by || resolveBy(cwd, env);
|
|
199
|
+
gitHard(['add', '--', itemPath], cwd, env);
|
|
200
|
+
const subject = `advance: triage ${item} → settled (by ${by})`;
|
|
201
|
+
gitHard(
|
|
202
|
+
[
|
|
203
|
+
'commit',
|
|
204
|
+
'--quiet',
|
|
205
|
+
'-m',
|
|
206
|
+
subject,
|
|
207
|
+
'-m',
|
|
208
|
+
`triaged: ${value}\n\n${reason}`,
|
|
209
|
+
],
|
|
210
|
+
cwd,
|
|
211
|
+
env,
|
|
212
|
+
);
|
|
213
|
+
const commit = gitHard(['rev-parse', 'HEAD'], cwd, env).stdout.trim();
|
|
214
|
+
const message =
|
|
215
|
+
`triage ${item}: back-stamped triaged:${value} (the note was already ` +
|
|
216
|
+
'resolved-and-kept — it carries an engine-written applied-answers record) ' +
|
|
217
|
+
'so the triage rung stops re-asking its settled question.';
|
|
218
|
+
note(message);
|
|
219
|
+
return {commit, message};
|
|
220
|
+
}
|
|
221
|
+
|
|
123
222
|
// --- The conservative auto-disposition (US #17) ---------------------------
|
|
124
223
|
|
|
125
224
|
export interface AutoDispositionOptions {
|
|
@@ -149,8 +248,10 @@ export interface AutoDispositionResult {
|
|
|
149
248
|
* DELETION (`git rm` in a standalone commit, the reason in the message). A
|
|
150
249
|
* `duplicate` is a redundant copy of `existing`; a `map` is already covered by
|
|
151
250
|
* `existing` — either way the note carries no unique signal, so it leaves the
|
|
152
|
-
* inbox by being gone.
|
|
153
|
-
* `agentic-apply-retire-disposition-vocabulary`)
|
|
251
|
+
* inbox by being gone. An AUTO disposition never RESTS a note (task
|
|
252
|
+
* `agentic-apply-retire-disposition-vocabulary`); resting a KEPT note is the
|
|
253
|
+
* human-answered `resolve` verdict's job on the apply rung (ADR
|
|
254
|
+
* `resolve-settles-the-question-loop-not-the-note`). NEVER an auto-delete of a
|
|
154
255
|
* NON-redundant signal.
|
|
155
256
|
*/
|
|
156
257
|
outcome: 'deleted';
|
|
@@ -167,9 +268,9 @@ export interface AutoDispositionResult {
|
|
|
167
268
|
* ONE local commit, no question surfaced. BOTH no-question cases DISCHARGE the
|
|
168
269
|
* redundant note BY DELETION (`git rm` in a STANDALONE commit, the mapped/
|
|
169
270
|
* duplicated-of identity + reason in the commit message; git history = archive).
|
|
170
|
-
*
|
|
171
|
-
* `agentic-apply-retire-disposition-vocabulary`
|
|
172
|
-
* acted-on, or deleted
|
|
271
|
+
* An AUTO disposition never RESTS a note (task
|
|
272
|
+
* `agentic-apply-retire-disposition-vocabulary`): its two cases carry no unique
|
|
273
|
+
* signal, so they are still-open, acted-on, or deleted — never kept:
|
|
173
274
|
*
|
|
174
275
|
* - `duplicate` → the note is an EXACT duplicate of `existing` (already
|
|
175
276
|
* captured); the original carries the signal, so the copy is deleted; or
|