@sublang/playbook 15.0.0 → 16.0.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/README.md +1 -1
- package/docs/embedding.md +9 -7
- package/package.json +3 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +191 -0
- package/slc/prefix.md +203 -0
- package/slc/slc.pin-inputs.json +11 -1
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +82 -9
- package/src/xstate-playbook-runtime.js +1224 -283
- package/src/xstate-playbook-runtime.ts +1671 -338
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
package/slc/gears2fsm.md
CHANGED
|
@@ -141,9 +141,11 @@ generic Captain forms, wire `<boss-intent>` from `bossIntent`,
|
|
|
141
141
|
`remainingPlan`, and `<completed-call-results>` from
|
|
142
142
|
`completedCallResults`. Other non-identity placeholders shall retain the
|
|
143
143
|
semantic typed field established by Source (for example `<#>` from
|
|
144
|
-
`irNumber`)
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
`irNumber`); a created-commit or labelled-section placeholder binds as
|
|
145
|
+
[Context and prompts](#context-and-prompts) states. Leaving an ordinary
|
|
146
|
+
runtime-value placeholder literal, replacing it with an empty default because
|
|
147
|
+
its field was omitted, or making the linker recover it from untyped context is
|
|
148
|
+
malformed.
|
|
147
149
|
The corresponding `invoke.input` object shall include that field beside `prompt`; storing it only in machine context does not satisfy the actor-input contract.
|
|
148
150
|
When Source declares a placeholder as the current identity of a local acting
|
|
149
151
|
role, preserve the placeholder literal in the FSM prompt and do not add any
|
|
@@ -610,6 +612,37 @@ or remove remaining entries as evidence arrives, but it cannot grow or retain
|
|
|
610
612
|
the same-length plan indefinitely; the initial finite array therefore bounds
|
|
611
613
|
the number of sequential child calls without an arbitrary runtime call limit.
|
|
612
614
|
|
|
615
|
+
A placeholder that reads the commit an earlier call created — `<code-commit>`,
|
|
616
|
+
`<decide-commit>` — binds to a typed context field named by its canonical
|
|
617
|
+
mapping, assigned from that call's accepted `latestCommit`, the effect-owned
|
|
618
|
+
property the runtime fills from the repository receipt
|
|
619
|
+
([text2gears](text2gears.md#result-contracts)); the actor input carries the
|
|
620
|
+
field beside the prompt like any other relayed value.
|
|
621
|
+
A placeholder the GEARS defines as a labelled section of another relayed
|
|
622
|
+
text binds to a typed context field named by its canonical mapping. The
|
|
623
|
+
definition names the label that opens the section and the labels that end it —
|
|
624
|
+
`<original-intent>` as the `Original intent:` section of the caller's request,
|
|
625
|
+
which runs to the `Review scope:` line or to the end of the request. The
|
|
626
|
+
machine derives the field deterministically in the entry action or transition
|
|
627
|
+
that stores the text and again in every action that replaces it, such as a
|
|
628
|
+
fresh entry directive or an interrupt that restarts with new text:
|
|
629
|
+
|
|
630
|
+
- the text as read is the text itself, except that where every non-blank line
|
|
631
|
+
begins with `>`, each line is read without that marker and one optional space
|
|
632
|
+
after it, the literal quote layer a quoted relay adds;
|
|
633
|
+
- the section is the lines of the text as read from the first line that
|
|
634
|
+
begins with the opening label, the label removed, through the line before the
|
|
635
|
+
first later line that begins with an ending label, or through the last line,
|
|
636
|
+
with the result trimmed; a line that merely looks labelled, such as `Note:`
|
|
637
|
+
or `Constraints:`, stays in the section;
|
|
638
|
+
- where no line begins with the opening label, or the section is empty once
|
|
639
|
+
trimmed — its label directly followed by an ending label or by the end of
|
|
640
|
+
the text — the field is the whole text as read, trimmed, so the request is
|
|
641
|
+
never lost.
|
|
642
|
+
|
|
643
|
+
The derived field is ordinary context that actor inputs relay, never a player
|
|
644
|
+
or judge output.
|
|
645
|
+
|
|
613
646
|
Prompts shall pass only the **specific extracted fields** the player needs.
|
|
614
647
|
The compiler shall not dump `JSON.stringify(lastResult)` or any opaque blob: it leaks internal `guard` strings, wastes tokens, and confuses the LLM.
|
|
615
648
|
|
|
@@ -663,6 +696,26 @@ The compiler shall update or reset those source-owned facts only at their actual
|
|
|
663
696
|
Transitions shall be self-driving when source items define the next obligation.
|
|
664
697
|
Routing to an idle hub is for recovery, unrecoverable Boss input, or one-shot entry events — not the happy path.
|
|
665
698
|
|
|
699
|
+
Where an artifact-schema-3 delegated-player state's outcomes are governed by the
|
|
700
|
+
linked runtime ([link.md "Captain adjudication"](link.md#captain-adjudication)),
|
|
701
|
+
every accepted `onDone` arm — each arm that routes a declared outcome, not the
|
|
702
|
+
malformed-output fallback — shall carry, first among its actions, the
|
|
703
|
+
root-machine action
|
|
704
|
+
`{ type: 'playbook.acceptedOutcome', params: { source: '<stateId>', target: '<target stateId>', acceptedOutcome: '<guard>' } }`,
|
|
705
|
+
with the setup's `actions` declaring `'playbook.acceptedOutcome'` as a no-op
|
|
706
|
+
that types those three string params.
|
|
707
|
+
`target` names the state the next public snapshot shows for the arm: the arm's
|
|
708
|
+
own target, or the parallel parent's `onDone` target where the arm's target is a
|
|
709
|
+
region's final leaf that completes that parent, because the machine leaves that
|
|
710
|
+
leaf before the snapshot; an arm that completes the join only when every
|
|
711
|
+
sibling region is already final therefore splits into two arms guarded on that
|
|
712
|
+
condition, each naming the state it reaches.
|
|
713
|
+
The linked runtime retains the marker until the next public snapshot confirms
|
|
714
|
+
`source` in the prior snapshot and `target` in the new one, and only then
|
|
715
|
+
publishes the outcome's accepted trace and `→ <acceptedOutcome>` status; an arm
|
|
716
|
+
without the marker accepts its outcome silently, and the host counts none of the
|
|
717
|
+
work it saved.
|
|
718
|
+
|
|
666
719
|
### Auto-advance on approval
|
|
667
720
|
|
|
668
721
|
A review/approval state's success outcome shall **target the next workflow step**, not idle back to a hub.
|
|
@@ -811,6 +864,12 @@ Where several questions are pending, the classifier prompt and event shall
|
|
|
811
864
|
require `questionId` and shall reject an omitted or unknown id without moving
|
|
812
865
|
the FSM.
|
|
813
866
|
|
|
867
|
+
Every exit from a Captain- or player-invoking state other than into its own
|
|
868
|
+
Boss-reply wait — an accepted outcome, the malformed-output fallback, an actor
|
|
869
|
+
error — and every non-reply exit from a wait shall clear that state's pending
|
|
870
|
+
question and reply context, so a call that fails after resuming leaves no
|
|
871
|
+
stale question for the next turn's classifier to offer.
|
|
872
|
+
|
|
814
873
|
The scalar `awaitBossReply` state and every local branch wait are quiescent for
|
|
815
874
|
the runtime drive boundary.
|
|
816
875
|
They shall allow a fresh root entry event or interrupt to abandon the relevant
|
|
@@ -882,6 +941,16 @@ Where Source declares a JSON-safe terminal result, the setup types shall
|
|
|
882
941
|
declare that output and the root machine shall derive it from typed context
|
|
883
942
|
through XState's machine `output` function. A final-state transition alone does
|
|
884
943
|
not satisfy a declared output contract.
|
|
944
|
+
Where the selected pipeline supplies the
|
|
945
|
+
[workflow contracts](workflow-contracts.json) catalog and the compiled Source's
|
|
946
|
+
basename is one of its `literalTargetBindings`, the declared terminal output
|
|
947
|
+
shall be exactly that builtin's public interface — its variants, `status`
|
|
948
|
+
constants, property names, and requiredness — derived from typed context,
|
|
949
|
+
because callers compiled against the catalog consume it by those names; an
|
|
950
|
+
authored outcome the interface cannot express is an inconsistency between the
|
|
951
|
+
Source and the catalog, reported as an incompatible compiler input under
|
|
952
|
+
[Nested playbook calls](#nested-playbook-calls) rather than expressed by an
|
|
953
|
+
invented or renamed outcome.
|
|
885
954
|
Fields that Source requires in every terminal output shall be required in the
|
|
886
955
|
TypeScript output type. In particular, a declared `{ response }` result shall
|
|
887
956
|
compile as `{ response: string }`, not `{ response?: string }`; reaching the
|
package/slc/link.md
CHANGED
|
@@ -38,15 +38,20 @@ The link compiler shall not modify the FSM artifact and shall not re-derive Capt
|
|
|
38
38
|
|
|
39
39
|
## Optional deterministic materialization
|
|
40
40
|
|
|
41
|
-
For an ordinary
|
|
42
|
-
|
|
41
|
+
For an ordinary workflow using only `player` and `script` actors — flat, or
|
|
42
|
+
with root parallel groups of the [Parallel groups](gears2fsm.md#parallel-groups)
|
|
43
|
+
shape — the supported prompt profile below, shared remaining strategies, and primitive configured options, the linker may
|
|
43
44
|
use the adjacent `materialize-link.mjs` tool to emit the thin module.
|
|
44
45
|
The complete definition below remains binding; the tool replaces repetitive
|
|
45
46
|
module generation, not semantic analysis or emitted conformance verification.
|
|
46
47
|
Read the actual FSM and supply every erased or authored contract exactly.
|
|
47
48
|
Do not use `flat-defaults` or `flat-quoted-relays` when another custom composer, classifier, required-field
|
|
48
49
|
extractor, session-derived input mapping, controller strategy, nested call,
|
|
49
|
-
|
|
50
|
+
or compound state other than such a parallel group is needed.
|
|
51
|
+
Despite their names, every profile accepts those parallel groups: the tool
|
|
52
|
+
takes each region working leaf's role and label under its stable state id
|
|
53
|
+
like any other player state, declares nothing further, and leaves the
|
|
54
|
+
compiled shape to the factory preflight, which rejects any other form.
|
|
50
55
|
|
|
51
56
|
Invoke the tool with the actual definition directory, source FSM, and declared
|
|
52
57
|
target; supply a JSON descriptor on standard input:
|
|
@@ -101,6 +106,12 @@ session-derived input values are outside this profile; do not widen their
|
|
|
101
106
|
contracts to an unconstrained primitive.
|
|
102
107
|
The tool adds optional string `cwd` for script-bearing machines; it does not
|
|
103
108
|
put `cwd` in FSM input unless the descriptor explicitly maps it.
|
|
109
|
+
When the linked module is part of a package that compiles and ships
|
|
110
|
+
JavaScript siblings, supply the optional `fsmSpecifier` — the `.js` sibling
|
|
111
|
+
the build emits for the source FSM, such as `./code.fsm.js`, in the same
|
|
112
|
+
directory and with the same basename — so the emitted import satisfies the
|
|
113
|
+
NodeNext rule under Output; omit it for a source-only host, where the tool
|
|
114
|
+
derives the specifier from the `--fsm` path.
|
|
104
115
|
An explicit `entryEvent: null` selects the shared classifier only where this
|
|
105
116
|
definition permits no deterministic entry; it does not relax entry rules.
|
|
106
117
|
`bossEvents` retains the exact additional erased event fields, source ownership,
|
|
@@ -125,8 +136,9 @@ correct the identified problem before treating linking as successful.
|
|
|
125
136
|
|
|
126
137
|
### Experimental labelled-string and nested-call profile
|
|
127
138
|
|
|
128
|
-
`flat-labelled-relays` is an unmeasured candidate for flat
|
|
129
|
-
|
|
139
|
+
`flat-labelled-relays` is an unmeasured candidate for machines with flat
|
|
140
|
+
root states or root parallel groups of that shape, whose delegated players,
|
|
141
|
+
scripts, and nested `playbook` calls it supports. It
|
|
130
142
|
requires the installed shared `composePlayerContinuation` API. It leaves
|
|
131
143
|
nested input composition, child targets, result guards, recovery, and terminal
|
|
132
144
|
semantics in the unchanged FSM; the shared factory provides the nested bridge.
|
|
@@ -154,8 +166,8 @@ source tokens. Undeclared empty relay lines retain their authored text.
|
|
|
154
166
|
The emitted verification composer has the same identity and optional third
|
|
155
167
|
resume arguments as the runtime; the shared helper owns fresh/resumed Q&A.
|
|
156
168
|
Structured rendering, custom classifiers/extractors/controllers, direct
|
|
157
|
-
Captain actors, compound
|
|
158
|
-
options remain unsupported. Source-derived metadata and all existing
|
|
169
|
+
Captain actors, compound topology other than such a parallel group, and
|
|
170
|
+
constrained/nonprimitive options remain unsupported. Source-derived metadata and all existing
|
|
159
171
|
conformance checks remain mandatory. This profile changes no runtime bridge,
|
|
160
172
|
workflow semantics, or measured performance claim.
|
|
161
173
|
|
|
@@ -1239,7 +1251,7 @@ undeclared extra key, so hidden adjudication cannot replace, paraphrase, or
|
|
|
1239
1251
|
decorate prose Boss already saw.
|
|
1240
1252
|
For an artifact-schema-3 governed delegated-player call, the shared engine
|
|
1241
1253
|
shall instead use one semantic reconciler for both the default linked runtime
|
|
1242
|
-
and any
|
|
1254
|
+
and any linked machinery of an artifact's own that adopts schema `3`.
|
|
1243
1255
|
That reconciler shall retain the validated player's exact non-empty
|
|
1244
1256
|
`finalText` as opaque presentation evidence, let the hidden adjudicator read
|
|
1245
1257
|
it only as semantic evidence, and require the adjudicator's detached
|
|
@@ -1257,8 +1269,8 @@ authored placeholder or guidance, with every presentation-, effect-, or
|
|
|
1257
1269
|
runtime-owned field named as runtime-supplied to omit — so the judge is never asked for a
|
|
1258
1270
|
field it does not own; the artifact's description text stays unchanged.
|
|
1259
1271
|
The shared engine shall export that rendering as `renderGovernedOutcomeContract`
|
|
1260
|
-
on `@sublang/playbook/xstate-runtime`, and
|
|
1261
|
-
render its judge prompt through it rather than restate the contract.
|
|
1272
|
+
on `@sublang/playbook/xstate-runtime`, and linked machinery of an artifact's
|
|
1273
|
+
own shall render its judge prompt through it rather than restate the contract.
|
|
1262
1274
|
The reconciler shall construct the complete actor output rather than accept a
|
|
1263
1275
|
cross-authority object from the judge: every presentation-owned payload field
|
|
1264
1276
|
shall receive the canonical `finalText.trim()` value; every effect-owned
|
|
@@ -1960,9 +1972,11 @@ A linked runtime may implement the optional adoption capability of
|
|
|
1960
1972
|
initialization path distinct from `init` and same-engagement `restore`.
|
|
1961
1973
|
Adoption may bind a retained generation to a fresh valid `PlaybookSession`
|
|
1962
1974
|
identity. Every runtime the shared `createXStatePlaybookRuntime` factory
|
|
1963
|
-
constructs implements `adopt`, regardless of whether the
|
|
1964
|
-
retained-generation classification metadata;
|
|
1965
|
-
|
|
1975
|
+
constructs for a flat machine implements `adopt`, regardless of whether the
|
|
1976
|
+
artifact supplies retained-generation classification metadata; one it
|
|
1977
|
+
constructs for a machine that declares a parallel state omits it, as linked
|
|
1978
|
+
machinery of an artifact's own may, and hosts feature-detect the capability
|
|
1979
|
+
by member presence.
|
|
1966
1980
|
|
|
1967
1981
|
Before actor construction or any player-session-store, port, trace, status,
|
|
1968
1982
|
or telemetry effect, `adopt` shall validate and detach the target session, the
|
|
@@ -2053,9 +2067,9 @@ and detached from that declaration. Absence means the runtime contributes no
|
|
|
2053
2067
|
retained generation; presence supplies only terminal classification metadata
|
|
2054
2068
|
and does not itself supply the adoption operation.
|
|
2055
2069
|
Every runtime the shared `createXStatePlaybookRuntime` factory constructs from
|
|
2056
|
-
a supplied `unfinishedFinalStateIds` spec member shall expose the marker;
|
|
2057
|
-
|
|
2058
|
-
itself.
|
|
2070
|
+
a supplied `unfinishedFinalStateIds` spec member shall expose the marker;
|
|
2071
|
+
linked machinery of an artifact's own opts into classification only by
|
|
2072
|
+
implementing the public member itself.
|
|
2059
2073
|
|
|
2060
2074
|
## Control surface (optional)
|
|
2061
2075
|
|
|
@@ -2387,14 +2401,19 @@ Every linked artifact shall emit an `unfinishedFinalStateIds` set beside its res
|
|
|
2387
2401
|
The set shall contain exactly the stable ids of root `type: 'final'` states whose terminal outcomes leave the procedure unfinished, and shall be explicitly empty when no terminal outcome does.
|
|
2388
2402
|
The linker shall not infer the set from a state description, opaque output, or procedure prose.
|
|
2389
2403
|
The linker shall reject a declared id that does not name a root final state, and the shared factory shall independently reject it at construction before runtime effects.
|
|
2390
|
-
For a factory-backed artifact the set is a `spec` member;
|
|
2391
|
-
For
|
|
2392
|
-
|
|
2404
|
+
For a factory-backed artifact the set is a `spec` member; linked machinery of an artifact's own shall retain equivalent linked metadata, and the artifact declaration is not itself the public runtime retention marker or an adoption capability.
|
|
2405
|
+
For every FSM [gears2fsm.md](gears2fsm.md) produces — flat under its
|
|
2406
|
+
one-state-per-item mapping, or carrying root parallel groups of its
|
|
2407
|
+
[Parallel groups](gears2fsm.md#parallel-groups) shape — the linker shall
|
|
2393
2408
|
emit the thin shared-factory module defined below.
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2409
|
+
The shared factory interprets such a parallel group through its parallel
|
|
2410
|
+
profile under [DR-067](../specs/decisions/067-parallel-proposals-through-the-shared-factory.md), deriving the regions, their leaves, and the
|
|
2411
|
+
cohort of working leaves from the FSM and its exported `concurrentRoleSets`,
|
|
2412
|
+
so the emitted module declares nothing further for it; bespoke linked
|
|
2413
|
+
machinery is no longer the prescribed output for any such FSM.
|
|
2414
|
+
Linked machinery of an artifact's own that satisfies this document's runtime
|
|
2415
|
+
contract remains a valid implementation for an artifact outside this package
|
|
2416
|
+
and declares the `bespoke` registry profile below, so hosts keep accepting it.
|
|
2398
2417
|
The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
|
|
2399
2418
|
mapping, adjudication, script execution, nested-playbook bridging, session
|
|
2400
2419
|
lifecycle, abort handling, and the optional parked-session snapshot and
|
|
@@ -2510,8 +2529,9 @@ The thin emitted module:
|
|
|
2510
2529
|
error.
|
|
2511
2530
|
`NO_ACTION` and `BOSS_REPLY` are runtime-owned event types the factory
|
|
2512
2531
|
supplies itself — `NO_ACTION` as exactly `{ type: 'NO_ACTION' }`, and
|
|
2513
|
-
`BOSS_REPLY` as
|
|
2514
|
-
|
|
2532
|
+
`BOSS_REPLY` as a judge-selected `questionId` — optional while one question
|
|
2533
|
+
is pending and required, from the pending ids, while several are — plus the
|
|
2534
|
+
exact-text `answer` the runtime attaches. `bossEvents` shall carry no entry for either
|
|
2515
2535
|
type; supplying one is a construction error, so a linker that judges a
|
|
2516
2536
|
runtime-owned arm to have lost payload detail under erasure shall report
|
|
2517
2537
|
that gap rather than emit the entry.
|
|
@@ -2553,8 +2573,11 @@ The thin emitted module:
|
|
|
2553
2573
|
host-construction boundary's setup diagnostic.
|
|
2554
2574
|
- Exposes, under an `_internal` export, the pure helpers verification
|
|
2555
2575
|
needs — at least the prompt composers its own machine uses, which may
|
|
2556
|
-
re-export the shared defaults when the spec does not override composition
|
|
2557
|
-
|
|
2576
|
+
re-export the shared defaults when the spec does not override composition,
|
|
2577
|
+
and the `VERBATIM_PAYLOAD_FIELDS` and `UNFINISHED_FINAL_STATE_IDS` sets the
|
|
2578
|
+
spec declares, which the linked-ownership check reads back — so
|
|
2579
|
+
compilation-correctness tests can exercise composition and ownership
|
|
2580
|
+
without a host.
|
|
2558
2581
|
A playbook that calls players exposes `composePlayerPrompt`; a playbook
|
|
2559
2582
|
whose states make direct-Captain calls exposes `composeCaptainPrompt`. A
|
|
2560
2583
|
controller playbook that calls no players exposes no player composer:
|
package/slc/materialize-link.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import { lstat, open, realpath, rename, unlink } from 'node:fs/promises';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
-
import { dirname, extname, relative, resolve } from 'node:path';
|
|
10
|
+
import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
|
|
13
13
|
// Compile-time source only; emitted modules depend only on the shared engine.
|
|
@@ -78,6 +78,10 @@ const DESCRIPTOR_KEYS = [
|
|
|
78
78
|
'transitionEventFields', 'verbatimPayloadFields', 'resumableStateIds',
|
|
79
79
|
'unfinishedFinalStateIds', 'controlContextFields',
|
|
80
80
|
];
|
|
81
|
+
// Optional: the runtime specifier of the FSM's emitted JavaScript sibling, for
|
|
82
|
+
// a module inside a package that compiles and ships `.js` beside its sources.
|
|
83
|
+
const OPTIONAL_KEYS = ['fsmSpecifier'];
|
|
84
|
+
const JS_SPECIFIER = /^\.\.?\/.+\.js$/;
|
|
81
85
|
const LABELLED_KEYS = ['playerInputExport', 'omitEmptyRelayLines', 'identityPlaceholders'];
|
|
82
86
|
const TOKEN = /^(#|[A-Za-z_$][A-Za-z0-9_$-]*)$/;
|
|
83
87
|
const CONTRACT_TYPES = [
|
|
@@ -126,7 +130,10 @@ function validateDescriptor(value, engine) {
|
|
|
126
130
|
const descriptor = engine.snapshotJsonValue(value, 'link descriptor');
|
|
127
131
|
const labelled = descriptor?.profile === 'flat-labelled-relays';
|
|
128
132
|
const keys = labelled ? [...DESCRIPTOR_KEYS, ...LABELLED_KEYS] : DESCRIPTOR_KEYS;
|
|
129
|
-
record(descriptor, 'descriptor', keys, keys);
|
|
133
|
+
record(descriptor, 'descriptor', [...keys, ...OPTIONAL_KEYS], keys);
|
|
134
|
+
if (descriptor.fsmSpecifier !== undefined && (typeof descriptor.fsmSpecifier !== 'string' || !JS_SPECIFIER.test(descriptor.fsmSpecifier))) {
|
|
135
|
+
throw new TypeError('fsmSpecifier must be a relative runtime specifier ending in .js');
|
|
136
|
+
}
|
|
130
137
|
if (descriptor.schema !== SCHEMA) throw new TypeError(`descriptor.schema must equal ${SCHEMA}`);
|
|
131
138
|
if (!['flat-defaults', 'flat-quoted-relays', 'flat-labelled-relays'].includes(descriptor.profile)) throw new UnsupportedLinkProfile('profile requires ordinary normative linking');
|
|
132
139
|
if (labelled) {
|
|
@@ -228,27 +235,46 @@ function validateDescriptor(value, engine) {
|
|
|
228
235
|
function inspectMachine(machine, engine, labelled = false) {
|
|
229
236
|
const config = machine?.config;
|
|
230
237
|
if (!config || typeof config !== 'object' || !config.states) throw new TypeError('FSM export must be an XState machine');
|
|
231
|
-
if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('
|
|
238
|
+
if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('a parallel machine root or root invocation requires ordinary linking');
|
|
232
239
|
const descriptions = engine.stateDescriptionsFromMachine(machine);
|
|
233
240
|
const roleStates = Object.create(null);
|
|
234
241
|
let hasScript = false;
|
|
235
|
-
|
|
242
|
+
const children = (value) => value && typeof value === 'object' ? Object.entries(value) : [];
|
|
243
|
+
const inspectState = (key, state) => {
|
|
236
244
|
if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
|
|
237
245
|
if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
|
|
238
|
-
if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
|
|
239
|
-
throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
|
|
240
|
-
}
|
|
241
246
|
const invokes = Array.isArray(state.invoke) ? state.invoke : state.invoke ? [state.invoke] : [];
|
|
242
247
|
if (invokes.length > 1) throw new UnsupportedLinkProfile(`state ${key} has multiple actors`);
|
|
243
248
|
for (const invoke of invokes) {
|
|
244
249
|
if (!(labelled ? ['player', 'script', 'playbook'] : ['player', 'script']).includes(invoke.src)) throw new UnsupportedLinkProfile(`state ${key} actor ${String(invoke.src)} requires ordinary linking`);
|
|
245
250
|
if (invoke.src === 'script') hasScript = true;
|
|
246
251
|
else if (invoke.src === 'player') {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
// The engine keys a player state by its stable playbook id, which a
|
|
253
|
+
// parallel region's leaf carries distinct from its local key.
|
|
254
|
+
const stateId = typeof state.meta?.playbook?.stateId === 'string' ? state.meta.playbook.stateId
|
|
255
|
+
: typeof state.id === 'string' ? state.id : key;
|
|
256
|
+
if (RESERVED_KEYS.has(stateId)) throw new UnsupportedLinkProfile(`state ${stateId} requires ordinary linking`);
|
|
257
|
+
const role = nonempty(state.meta?.playbook?.role, `state ${stateId} role`);
|
|
258
|
+
const label = nonempty(descriptions.get(stateId), `state ${stateId} description`);
|
|
259
|
+
roleStates[stateId] = { role, label };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
for (const [key, state] of Object.entries(config.states)) {
|
|
264
|
+
if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
|
|
265
|
+
if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
|
|
266
|
+
if (state.type === 'parallel') {
|
|
267
|
+
// DR-067: walk a root parallel state's region leaves; the engine's
|
|
268
|
+
// construction preflight below owns validation of the compiled shape.
|
|
269
|
+
for (const [, region] of children(state.states)) {
|
|
270
|
+
for (const [leafKey, leaf] of children(region?.states)) inspectState(leafKey, leaf);
|
|
250
271
|
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
|
|
275
|
+
throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
|
|
251
276
|
}
|
|
277
|
+
inspectState(key, state);
|
|
252
278
|
}
|
|
253
279
|
return { roleStates, hasScript };
|
|
254
280
|
}
|
|
@@ -355,6 +381,7 @@ const OPTION_SCHEMA = ${json(options)} as const;
|
|
|
355
381
|
const INPUT_MAPPING = ${json(descriptor.inputMapping)} as const;
|
|
356
382
|
const RESUMABLE_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.resumableStateIds)});
|
|
357
383
|
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.unfinishedFinalStateIds)});
|
|
384
|
+
const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set(${json(descriptor.verbatimPayloadFields)});
|
|
358
385
|
export function validateOptions(value: unknown): PlaybookRuntimeOptions {
|
|
359
386
|
const captured = snapshotJsonValue(value === undefined ? {} : value, ${JSON.stringify(`${descriptor.label} runtime options`)});
|
|
360
387
|
if (captured === null || typeof captured !== 'object' || Array.isArray(captured)) {
|
|
@@ -380,7 +407,7 @@ ${quotedPlayer || labelledPlayer ? ' composePlayerPrompt,\n' : ''} ...${json(d
|
|
|
380
407
|
Object.entries(INPUT_MAPPING).filter(([, key]) => options[key as keyof PlaybookRuntimeOptions] !== undefined)
|
|
381
408
|
.map(([field, key]) => [field, options[key as keyof PlaybookRuntimeOptions]]),
|
|
382
409
|
),
|
|
383
|
-
verbatimPayloadFields:
|
|
410
|
+
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
384
411
|
resumableStateIds: RESUMABLE_STATE_IDS,
|
|
385
412
|
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
386
413
|
} satisfies XStatePlaybookRuntimeSpecV3<PlaybookRuntimeOptions>;
|
|
@@ -389,6 +416,7 @@ export const _internal = {
|
|
|
389
416
|
${quotedPlayer || labelledPlayer ? 'composePlayerPrompt,\n ' : hasPlayer ? `composePlayerPrompt: (input: PlaybookPlayerInput, _identity?: unknown${hasContinuationMode ? ', resuming = false' : ''}) =>
|
|
390
417
|
defaultComposePlayerPrompt(input, ${json(descriptor.placeholderFields)}${hasContinuationMode ? ', resuming' : ''}),\n ` : ''}RESUMABLE_STATE_IDS,
|
|
391
418
|
UNFINISHED_FINAL_STATE_IDS,
|
|
419
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
392
420
|
};
|
|
393
421
|
const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
|
|
394
422
|
XStatePlaybookRuntimeConstruction<PlaybookRuntimeOptions, PlaybookHostCapabilities>, 3
|
|
@@ -428,9 +456,19 @@ export async function runMaterializer(argv, input) {
|
|
|
428
456
|
// Validate the export name before selecting it from the imported module.
|
|
429
457
|
validateDescriptor(descriptor, engine);
|
|
430
458
|
const module = await import(pathToFileURL(source).href);
|
|
431
|
-
const
|
|
459
|
+
const derived = relative(dirname(out), fsm).replaceAll('\\', '/');
|
|
460
|
+
let specifier = derived.startsWith('.') ? derived : `./${derived}`;
|
|
461
|
+
if (descriptor.fsmSpecifier !== undefined) {
|
|
462
|
+
// The declared `.js` sibling must be the build output of exactly this FSM:
|
|
463
|
+
// same directory and same basename, differing only in the extension.
|
|
464
|
+
const declared = resolve(dirname(out), descriptor.fsmSpecifier);
|
|
465
|
+
if (dirname(declared) !== dirname(fsm) || basename(declared, '.js') !== basename(fsm, extname(fsm))) {
|
|
466
|
+
throw new TypeError('fsmSpecifier must name the .js sibling of the --fsm file');
|
|
467
|
+
}
|
|
468
|
+
specifier = descriptor.fsmSpecifier;
|
|
469
|
+
}
|
|
432
470
|
const code = materializeLink({ machine: module[descriptor.machineExport], descriptor,
|
|
433
|
-
fsmSpecifier: specifier
|
|
471
|
+
fsmSpecifier: specifier, engine });
|
|
434
472
|
const temporary = `${out}.${randomUUID()}.tmp`;
|
|
435
473
|
try {
|
|
436
474
|
const file = await open(temporary, 'wx', 0o644);
|
package/slc/optimize.md
CHANGED
|
@@ -116,7 +116,10 @@ shell script are language-independent.
|
|
|
116
116
|
|
|
117
117
|
The pass shall append one `## Optimizations` section at the end of the target
|
|
118
118
|
listing every rewritten item, one bullet per item:
|
|
119
|
-
`- <ITEM-ID>:
|
|
119
|
+
`- <ITEM-ID>: captain → script`, where `captain` is the fixed token for the
|
|
120
|
+
direct-Captain behavior kind the pass rewrites — the only kind eligible — so
|
|
121
|
+
the section reads the same in every target and a consumer can match it
|
|
122
|
+
exactly.
|
|
120
123
|
When no item is eligible, the target shall be the source content unchanged,
|
|
121
124
|
with no `## Optimizations` section.
|
|
122
125
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
4
|
+
|
|
5
|
+
// Deterministic realization of the prompt-prefix pass (slc/prefix.md): every
|
|
6
|
+
// standalone relay block of an eligible item moves after its last instruction
|
|
7
|
+
// line, byte-for-byte, and nothing records the rewrite beside the prompts; a
|
|
8
|
+
// `## Prefixed prompts` section an earlier version of the pass appended is
|
|
9
|
+
// removed. `--keep <ITEM-ID>` excludes an item the pass judged ineligible; the
|
|
10
|
+
// tool never modifies the source.
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const HEADING = /^#{1,3}\s/;
|
|
17
|
+
const ITEM_HEADING = /^###\s+([^\s]+)\s*$/;
|
|
18
|
+
const PROMPT_LINE = /^>\s?(.*)$/;
|
|
19
|
+
const LEGACY_SECTION = /^##\s+Prefixed prompts\s*$/;
|
|
20
|
+
|
|
21
|
+
/** Classify one prompt line by its content inside the outer blockquote marker. */
|
|
22
|
+
function classify(line) {
|
|
23
|
+
const inner = PROMPT_LINE.exec(line)[1];
|
|
24
|
+
if (inner === '') return 'blank';
|
|
25
|
+
return inner.startsWith('>') ? 'relay' : 'instruction';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite one blockquote (its raw `> …` file lines). Returns the new lines, or
|
|
30
|
+
* null when no relay block precedes an instruction line.
|
|
31
|
+
*/
|
|
32
|
+
function prefixBlockquote(quote) {
|
|
33
|
+
const kinds = quote.map(classify);
|
|
34
|
+
// A relay block is a maximal run of relay lines bounded by blank lines or
|
|
35
|
+
// the blockquote edges; a relay line adjacent to an instruction line is
|
|
36
|
+
// authored content of that instruction and stays with it.
|
|
37
|
+
const inBlock = new Array(quote.length).fill(false);
|
|
38
|
+
for (let start = 0; start < quote.length; ) {
|
|
39
|
+
if (kinds[start] !== 'relay') {
|
|
40
|
+
start++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
let end = start;
|
|
44
|
+
while (end < quote.length && kinds[end] === 'relay') end++;
|
|
45
|
+
const boundedBefore = start === 0 || kinds[start - 1] === 'blank';
|
|
46
|
+
const boundedAfter = end === quote.length || kinds[end] === 'blank';
|
|
47
|
+
if (boundedBefore && boundedAfter) inBlock.fill(true, start, end);
|
|
48
|
+
start = end;
|
|
49
|
+
}
|
|
50
|
+
const firstBlock = inBlock.indexOf(true);
|
|
51
|
+
if (firstBlock === -1) return null;
|
|
52
|
+
const lastInstruction = Math.max(
|
|
53
|
+
...kinds.map((kind, index) =>
|
|
54
|
+
kind === 'blank' || inBlock[index] ? -1 : index,
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
if (lastInstruction < firstBlock) return null; // relays already trail
|
|
58
|
+
|
|
59
|
+
// Instruction lines keep their order and their authored blank lines; a
|
|
60
|
+
// removed block takes the blank lines bounding it and leaves the first of
|
|
61
|
+
// them where it separated two remaining lines, and none leads or trails the
|
|
62
|
+
// region.
|
|
63
|
+
const removed = [...inBlock];
|
|
64
|
+
inBlock.forEach((inside, index) => {
|
|
65
|
+
if (!inside) return;
|
|
66
|
+
for (let at = index - 1; at >= 0 && kinds[at] === 'blank'; at--) removed[at] = true;
|
|
67
|
+
for (let at = index + 1; at < quote.length && kinds[at] === 'blank'; at++) removed[at] = true;
|
|
68
|
+
});
|
|
69
|
+
const statics = [];
|
|
70
|
+
let separator;
|
|
71
|
+
for (let index = 0; index < quote.length; index++) {
|
|
72
|
+
if (removed[index]) {
|
|
73
|
+
separator ??= quote[index];
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (statics.length === 0 && kinds[index] === 'blank') continue;
|
|
77
|
+
if (separator !== undefined && statics.length > 0) statics.push(separator);
|
|
78
|
+
separator = undefined;
|
|
79
|
+
statics.push(quote[index]);
|
|
80
|
+
}
|
|
81
|
+
while (statics.length > 0 && classify(statics[statics.length - 1]) === 'blank') {
|
|
82
|
+
statics.pop();
|
|
83
|
+
}
|
|
84
|
+
const blocks = [];
|
|
85
|
+
for (let index = 0; index < quote.length; index++) {
|
|
86
|
+
if (!inBlock[index]) continue;
|
|
87
|
+
if (index === 0 || !inBlock[index - 1]) blocks.push([]);
|
|
88
|
+
blocks[blocks.length - 1].push(quote[index]);
|
|
89
|
+
}
|
|
90
|
+
const relays = blocks.flatMap((block, index) =>
|
|
91
|
+
index === 0 ? block : ['>', ...block],
|
|
92
|
+
);
|
|
93
|
+
return [...statics, '>', ...relays];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Rewrite every eligible item of a GEARS package not named by `keep` and
|
|
98
|
+
* remove any legacy `## Prefixed prompts` section. Returns the target text —
|
|
99
|
+
* the source text itself when neither applies — and the rewritten item ids.
|
|
100
|
+
*/
|
|
101
|
+
export function prefixPrompts(text, { keep = [] } = {}) {
|
|
102
|
+
// Keep the source's own line-ending convention so untouched lines stay bytes.
|
|
103
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
104
|
+
const lines = text.split(/\r?\n/);
|
|
105
|
+
const starts = [];
|
|
106
|
+
lines.forEach((line, index) => {
|
|
107
|
+
const match = ITEM_HEADING.exec(line);
|
|
108
|
+
if (match !== null) starts.push({ index, id: match[1] });
|
|
109
|
+
});
|
|
110
|
+
const out = [];
|
|
111
|
+
let cursor = 0;
|
|
112
|
+
const rewritten = [];
|
|
113
|
+
for (const start of starts) {
|
|
114
|
+
let end = start.index + 1;
|
|
115
|
+
while (end < lines.length && !HEADING.test(lines[end])) end++;
|
|
116
|
+
let quoteStart = start.index + 1;
|
|
117
|
+
while (quoteStart < end && !PROMPT_LINE.test(lines[quoteStart])) quoteStart++;
|
|
118
|
+
if (quoteStart === end) continue;
|
|
119
|
+
let quoteEnd = quoteStart;
|
|
120
|
+
while (quoteEnd < end && PROMPT_LINE.test(lines[quoteEnd])) quoteEnd++;
|
|
121
|
+
// A script item's blockquote is shell text, not a prompt (optimize.md).
|
|
122
|
+
const acting = lines.slice(start.index + 1, quoteStart).join(' ');
|
|
123
|
+
if (/\bCaptain shall run:/.test(acting) || keep.includes(start.id)) continue;
|
|
124
|
+
const replaced = prefixBlockquote(lines.slice(quoteStart, quoteEnd));
|
|
125
|
+
if (replaced === null) continue;
|
|
126
|
+
out.push(...lines.slice(cursor, quoteStart), ...replaced);
|
|
127
|
+
cursor = quoteEnd;
|
|
128
|
+
rewritten.push(start.id);
|
|
129
|
+
}
|
|
130
|
+
out.push(...lines.slice(cursor));
|
|
131
|
+
// A legacy section runs to the next heading; at the end of the file it
|
|
132
|
+
// takes the blank lines before it too, so the file ends as it did before
|
|
133
|
+
// the section was appended.
|
|
134
|
+
let removed = false;
|
|
135
|
+
for (let index = out.length - 1; index >= 0; index--) {
|
|
136
|
+
if (!LEGACY_SECTION.test(out[index])) continue;
|
|
137
|
+
let start = index;
|
|
138
|
+
let end = index + 1;
|
|
139
|
+
while (end < out.length && !HEADING.test(out[end])) end++;
|
|
140
|
+
if (end === out.length) {
|
|
141
|
+
while (start > 0 && out[start - 1] === '') start--;
|
|
142
|
+
out.splice(start, end - start, '');
|
|
143
|
+
} else {
|
|
144
|
+
out.splice(start, end - start);
|
|
145
|
+
}
|
|
146
|
+
removed = true;
|
|
147
|
+
}
|
|
148
|
+
if (rewritten.length === 0 && !removed) return { text, rewritten };
|
|
149
|
+
return { text: out.join(newline), rewritten };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main(argv) {
|
|
153
|
+
let source;
|
|
154
|
+
let target;
|
|
155
|
+
const keep = [];
|
|
156
|
+
for (let index = 0; index < argv.length; index++) {
|
|
157
|
+
const flag = argv[index];
|
|
158
|
+
const value = argv[index + 1];
|
|
159
|
+
if (value === undefined || value.startsWith('--')) {
|
|
160
|
+
throw new Error(`${flag} needs a value`);
|
|
161
|
+
}
|
|
162
|
+
if (flag === '--source') source = value;
|
|
163
|
+
else if (flag === '--target') target = value;
|
|
164
|
+
else if (flag === '--keep') keep.push(value);
|
|
165
|
+
else throw new Error(`unknown argument ${flag}`);
|
|
166
|
+
index++;
|
|
167
|
+
}
|
|
168
|
+
if (source === undefined || target === undefined) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'usage: prefix-prompts.mjs --source <gears.md> --target <gears.md> [--keep <ITEM-ID>]...',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const input = await readFile(resolve(source), 'utf8');
|
|
174
|
+
const { text, rewritten } = prefixPrompts(input, { keep });
|
|
175
|
+
await writeFile(resolve(target), text);
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
rewritten.length > 0
|
|
178
|
+
? `${rewritten.join('\n')}\n`
|
|
179
|
+
: text === input
|
|
180
|
+
? 'no eligible item; target equals source\n'
|
|
181
|
+
: 'no eligible item; legacy ## Prefixed prompts section removed\n',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const invokedPath = process.argv[1] === undefined ? undefined : resolve(process.argv[1]);
|
|
186
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
187
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
188
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
});
|
|
191
|
+
}
|