@yagni-app/code 0.3.2 → 0.3.3

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.
Files changed (58) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/extension/footer.d.ts +1 -1
  3. package/dist/extension/hooks.d.ts +111 -0
  4. package/dist/extension/hooks.js +666 -0
  5. package/dist/extension/index.d.ts +13 -6
  6. package/dist/extension/index.js +57 -7
  7. package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
  8. package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
  9. package/dist/extension/permission/dbReadPolicy.js +227 -0
  10. package/dist/extension/{execPolicy.js → permission/execPolicy.js} +41 -13
  11. package/dist/extension/{permission.d.ts → permission/gate.d.ts} +9 -2
  12. package/dist/extension/{permission.js → permission/gate.js} +103 -4
  13. package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
  14. package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
  15. package/dist/extension/permission/index.d.ts +14 -0
  16. package/dist/extension/permission/index.js +14 -0
  17. package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
  18. package/dist/extension/permission/packageManagerPolicy.js +170 -0
  19. package/dist/extension/pipeline/activityFeed.js +19 -5
  20. package/dist/extension/pipeline/checker.d.ts +99 -0
  21. package/dist/extension/pipeline/checker.js +238 -0
  22. package/dist/extension/pipeline/fanout.d.ts +116 -0
  23. package/dist/extension/pipeline/fanout.js +248 -0
  24. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  25. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  26. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  27. package/dist/extension/pipeline/goCommand.js +38 -1
  28. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  29. package/dist/extension/pipeline/headlessGo.js +333 -0
  30. package/dist/extension/pipeline/invocation.d.ts +7 -1
  31. package/dist/extension/pipeline/invocation.js +7 -1
  32. package/dist/extension/pipeline/mission.d.ts +55 -0
  33. package/dist/extension/pipeline/mission.js +70 -0
  34. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  35. package/dist/extension/pipeline/orchestrator.js +450 -9
  36. package/dist/extension/pipeline/personas.d.ts +16 -1
  37. package/dist/extension/pipeline/personas.js +117 -6
  38. package/dist/extension/pipeline/runSession.d.ts +45 -1
  39. package/dist/extension/pipeline/runState.d.ts +57 -12
  40. package/dist/extension/pipeline/runState.js +60 -18
  41. package/dist/extension/pipeline/runner.js +10 -1
  42. package/dist/extension/pipeline/stages.d.ts +84 -7
  43. package/dist/extension/pipeline/stages.js +166 -0
  44. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  45. package/dist/extension/pipeline/tierCap.js +57 -0
  46. package/dist/extension/pipeline/types.d.ts +130 -1
  47. package/dist/extension/pipeline/types.js +17 -0
  48. package/dist/extension/pipeline/verify.d.ts +86 -3
  49. package/dist/extension/pipeline/verify.js +175 -6
  50. package/dist/extension/turnLog.d.ts +38 -0
  51. package/dist/extension/turnLog.js +93 -0
  52. package/dist/goHeadless.d.ts +75 -0
  53. package/dist/goHeadless.js +132 -0
  54. package/dist/paths.d.ts +9 -0
  55. package/dist/paths.js +12 -0
  56. package/package.json +2 -2
  57. /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
  58. /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The implement diamond's RECORDING thread: pipeline progress in, run-session
3
+ * stage beats out (spec "Recording and the run surface").
4
+ *
5
+ * The pipeline already says everything the run surface needs — the partition
6
+ * verdict, each builder starting and finishing, each bounded fix turn — on the
7
+ * `onProgress` channel. This folds that stream into the additive
8
+ * `fanout` / `children` / `fixTurns` payloads on `POST /runs/:id/stages`, so the
9
+ * web run surface can render per-workstream rows without the pipeline growing a
10
+ * second reporting path.
11
+ *
12
+ * PURE and I/O-free, like `findings.ts` and `fanout.ts`: `apply` returns the beat
13
+ * to send (or null when the signal is not the diamond's), and the caller decides
14
+ * whether to POST it. Two properties are load-bearing:
15
+ *
16
+ * 1. **Recorded states only.** A child row exists only once its `stage_start`
17
+ * arrives, and it is `failed` only when the pipeline said the child died.
18
+ * There is no invented "pending", and no tick nobody earned.
19
+ * 2. **Every beat is a copy.** The roster keeps mutating as children finish, so
20
+ * a beat already handed to the (async, fail-soft) session must never change
21
+ * underneath it.
22
+ */
23
+ /** The stage every fan-out beat belongs to: the diamond lives INSIDE implement. */
24
+ const IMPLEMENT = "implement";
25
+ export function makeFanoutBeats() {
26
+ // Name -> tier + claim count from the partition verdict, so a child row can
27
+ // carry them the moment that child starts.
28
+ const shape = new Map();
29
+ const children = [];
30
+ const fixTurns = [];
31
+ const roster = () => children.map((c) => ({ ...c }));
32
+ const upsert = (name, state) => {
33
+ const existing = children.find((c) => c.name === name);
34
+ if (existing) {
35
+ existing.state = state;
36
+ }
37
+ else {
38
+ const meta = shape.get(name) ?? {};
39
+ children.push({
40
+ name,
41
+ ...(meta.tier ? { tier: meta.tier } : {}),
42
+ ...(meta.files !== undefined ? { files: meta.files } : {}),
43
+ state,
44
+ });
45
+ }
46
+ return { stage: IMPLEMENT, phase: "start", children: roster() };
47
+ };
48
+ return {
49
+ apply(p) {
50
+ switch (p.kind) {
51
+ case "fanout": {
52
+ for (const w of p.workstreams ?? [])
53
+ shape.set(w.name, { tier: w.tier, files: w.files });
54
+ return {
55
+ stage: IMPLEMENT,
56
+ phase: "start",
57
+ fanout: {
58
+ mode: p.mode,
59
+ ...(p.width ? { width: p.width } : {}),
60
+ reason: p.reason,
61
+ },
62
+ };
63
+ }
64
+ case "stage_start":
65
+ if (p.stageId !== IMPLEMENT || !p.workstream)
66
+ return null;
67
+ return upsert(p.workstream, "running");
68
+ case "stage_done":
69
+ if (p.stageId !== IMPLEMENT || !p.workstream)
70
+ return null;
71
+ return upsert(p.workstream, p.degraded === "failed" ? "failed" : "done");
72
+ case "fix_turn": {
73
+ fixTurns.push({ turn: p.turn, findings: p.findings, reengaged: [...p.reengaged] });
74
+ return {
75
+ stage: IMPLEMENT,
76
+ phase: "start",
77
+ fixTurns: fixTurns.map((t) => ({ ...t, reengaged: [...t.reengaged] })),
78
+ };
79
+ }
80
+ default:
81
+ return null;
82
+ }
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=fanoutBeats.js.map
@@ -114,6 +114,20 @@ export interface RegisterGoDeps {
114
114
  exists?: (path: string) => boolean;
115
115
  /** Clock seam for registry rows + staleness. */
116
116
  now?: () => number;
117
+ /**
118
+ * Environment the run-wide /go settings are read from (today: `go.fanout` via
119
+ * YAGNI_GO_FANOUT). Defaults to `process.env`; injected so a test can pin a
120
+ * benchmark lane without mutating the process.
121
+ */
122
+ env?: NodeJS.ProcessEnv;
123
+ /**
124
+ * Is /ultra on right now? The same holder the subagent tool reads (wired in
125
+ * index.ts), consulted per call because /ultra can flip mid-session. It is the
126
+ * implement diamond's parallel ceiling: ultra runs the fan (and its fix turns)
127
+ * up to 8 wide, everything else stays at 4. Absent (an unwired embedder or a
128
+ * test) simply means "not ultra", which is today's behavior.
129
+ */
130
+ isUltra?: () => boolean;
117
131
  baseUrl?: string;
118
132
  getToken?: () => string | undefined;
119
133
  /**
@@ -63,6 +63,8 @@ import { existsSync } from "node:fs";
63
63
  import { join } from "node:path";
64
64
  import { eventToLine } from "./activity.js";
65
65
  import { ActivityFeed, SPINNER_FRAMES } from "./activityFeed.js";
66
+ import { resolveFanoutMode } from "./fanout.js";
67
+ import { makeFanoutBeats } from "./fanoutBeats.js";
66
68
  import { RunState } from "./runState.js";
67
69
  import { aggregateRunUsage } from "./budget.js";
68
70
  import { formatRunCostTable } from "./runCostTable.js";
@@ -78,7 +80,7 @@ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
78
80
  import { composeAbortSignal } from "./resilience.js";
79
81
  import { planResume } from "./resume.js";
80
82
  import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, resolveMaxConcurrentRuns, settleRun, trackRunAbort, trackRunPromise, worktreesDir, } from "./runRegistry.js";
81
- import { makeRunSession as defaultMakeRunSession } from "./runSession.js";
83
+ import { makeRunSession as defaultMakeRunSession, } from "./runSession.js";
82
84
  import { recordSessionRun } from "../sessionRuns.js";
83
85
  import { resolveTicketBrief as defaultResolveTicketBrief } from "./ticketResolution.js";
84
86
  import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
@@ -422,6 +424,13 @@ export function registerGoCommand(pi, deps = {}) {
422
424
  const loadJournal = deps.loadJournal ?? ((sessionKey) => makeFileCheckpointStore(sessionKey).load());
423
425
  const exists = deps.exists ?? existsSync;
424
426
  const now = deps.now ?? Date.now;
427
+ // `go.fanout` (spec decision 8): `always` pins the implement diamond for
428
+ // benchmark / eval lanes; unset leaves the partitioner's conservative verdict
429
+ // as the only thing that decides.
430
+ const fanoutMode = resolveFanoutMode(deps.env ?? process.env);
431
+ // The dial is held, never its value: /ultra can be toggled between runs, so
432
+ // the pipeline calls it when it needs the ceiling rather than reading it here.
433
+ const isUltra = deps.isUltra;
425
434
  // Task 8: no default — see the RegisterGoDeps doc comment for why (only
426
435
  // index.ts's real wiring makes sense here; absent, `resolveCostText` falls
427
436
  // back to the local client estimate).
@@ -559,6 +568,10 @@ export function registerGoCommand(pi, deps = {}) {
559
568
  const feed = ctx.hasUI && !desktop ? new ActivityFeed(ticket) : undefined;
560
569
  const startedAt = Date.now();
561
570
  const run = ctx.hasUI && desktop ? new RunState({ runId: runShortId, ticket, startedAt }) : undefined;
571
+ // The implement diamond's recording thread. Per RUN (never module-scoped):
572
+ // it holds this run's child roster and fix turns. Independent of `feed` /
573
+ // `run`, because the Work page's run surface is fed on headless runs too.
574
+ const fanoutBeats = makeFanoutBeats();
562
575
  const stateKey = `${STATE_KEY_PREFIX}${runShortId}`;
563
576
  let paintTimer;
564
577
  let animationTimer;
@@ -639,6 +652,21 @@ export function registerGoCommand(pi, deps = {}) {
639
652
  baseUrl: deps.baseUrl ?? resolveBaseUrl(),
640
653
  getToken: deps.getToken ?? defaultGetToken,
641
654
  });
655
+ // The diamond's own recording channel is STRICTLY ORDERED, and it is the
656
+ // only one that has to be. Every roster beat carries the WHOLE roster and
657
+ // the run surface derives it last-one-wins (FanoutSection's `readFanout`),
658
+ // so two beats in flight at once can land in either order and a "running"
659
+ // that overtakes its own "done" paints a finished builder as running for
660
+ // the rest of the run. Chaining them through one promise makes the order
661
+ // the surface reads the order the pipeline produced. Still fire-and-forget
662
+ // (nothing on the run's critical path waits on it) and still fail-soft: a
663
+ // rejected beat never breaks the chain. The other stage beats stay
664
+ // unserialized on purpose — each names its own boundary, so their arrival
665
+ // order carries no state a later beat can undo.
666
+ let fanBeatChain = Promise.resolve();
667
+ const recordFanBeat = (b) => {
668
+ fanBeatChain = fanBeatChain.then(() => session.stage(b)).catch(() => { });
669
+ };
642
670
  const repoCtx = await resolveRepoContext(runCwd, ctx.signal);
643
671
  // Durable resilience journal, keyed by the RUN tree (the worktree path for
644
672
  // a default run; ctx.cwd for --here). Detect an interrupted prior /go for
@@ -880,10 +908,19 @@ export function registerGoCommand(pi, deps = {}) {
880
908
  let result = await runPipeline(ticket, {
881
909
  cwd: runCwd,
882
910
  signal: runSignal,
911
+ fanout: fanoutMode,
912
+ ...(isUltra ? { isUltra } : {}),
883
913
  ...(ticketBrief ? { ticketBrief } : {}),
884
914
  onProgress: (p) => {
885
915
  feed?.applyProgress(p);
886
916
  run?.applyProgress(p, Date.now());
917
+ // The implement diamond's own beats (the partition verdict, each
918
+ // child's state, each fix turn) ride the SAME fail-soft stage
919
+ // channel as every other boundary, additively: a signal that is not
920
+ // the diamond's produces no beat at all.
921
+ const fanBeat = fanoutBeats.apply(p);
922
+ if (fanBeat)
923
+ recordFanBeat(fanBeat);
887
924
  // The ribbon replaces the status chip on the desktop, so the chip is
888
925
  // only set for the terminal's status-line fallback.
889
926
  if (ctx.hasUI && !desktop) {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The headless front door to the /go pipeline (spec: sandbox harness parity).
3
+ *
4
+ * `/go` is an interactive pi slash command and pi's print mode ignores slash
5
+ * commands, so until now the ONLY way to drive the pipeline from a script was
6
+ * to import `runPipeline` directly (`scripts/eval-go-harness.ts` did exactly
7
+ * that). That gap is why the cloud mission sandbox ran a single flat model
8
+ * session instead of the product's staged pipeline. This module closes it:
9
+ *
10
+ * yagni go --headless --ticket-file <p> [--plan-file <p>] [--memo-file <p>]
11
+ * [--run-id <id>] [--cwd <p>] [--json]
12
+ *
13
+ * It is the SAME `runPipeline` path the interactive command drives (no forked
14
+ * driver) with the interactive-only machinery left out: no worktree, no run
15
+ * registry, no run session mint (the caller already owns the run row and passes
16
+ * its id with `--run-id`), no checkpoint journal, no terminal UI.
17
+ *
18
+ * MISSION MODE (`--plan-file`, optionally `--memo-file`): the factory already
19
+ * produced the plan and a human approved it at the mission's plan gate, so the
20
+ * pipeline skips its own map/plan stages and enters at implement on that plan,
21
+ * with the scoping memo as the repo context the map brief would have given (see
22
+ * mission.ts for the pure rules). Delivery is NOT ours in mission mode: the
23
+ * FINISH stage is a `/go`-session step and is never driven here, so a mission
24
+ * run ends at the reviewed candidate and never commits, pushes, opens a PR, or
25
+ * reports one. The mission's own prHandoff owns all of that.
26
+ *
27
+ * Output contract:
28
+ * - `--json`: one NDJSON line per event on stdout. Child events are the pi
29
+ * `--mode json` objects VERBATIM plus an additive `yagni` attribution key
30
+ * (stage / lens / round), so the event vocabulary is unchanged; pipeline-level
31
+ * lines carry their own `pipeline_*` types.
32
+ * - `{type:"pipeline_fanout", stage, phase, fanout?, children?, fixTurns?}`:
33
+ * the implement diamond's recording beats. Interactive `/go` POSTs these to
34
+ * its run session; headlessly there IS no session, so the SAME
35
+ * `makeFanoutBeats` thread rides the stream instead and the mission
36
+ * collector folds them back onto the Run. Without this a cloud mission would
37
+ * record no partition decision, no per-workstream row, and no fix turn.
38
+ * - Always, as the last line: the result object
39
+ * `{type:"pipeline_result", ok, stopReason, rounds, findings, stages:[{stage,
40
+ * tier, usage}], …}` — or `{type:"pipeline_error", …}` when a build stage
41
+ * threw.
42
+ * - Exit codes: 0 ONLY on a verified candidate (a `clean` stop), 1 for any
43
+ * other outcome or a thrown pipeline error, 2 for a usage/input problem.
44
+ *
45
+ * `YAGNI_GO_TIER_CAP` (eval + template-smoke lanes) is resolved here for the
46
+ * result object and forwarded on the child env; the clamp itself is applied
47
+ * centrally in `runStage` (see tierCap.ts). `YAGNI_GO_FANOUT` is read the same
48
+ * way and pins the implement diamond for a benchmark lane (see fanout.ts).
49
+ */
50
+ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
51
+ import type { RunBudget } from "./budget.js";
52
+ import type { JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
53
+ /** One-line usage copy, shared by every argument error. */
54
+ export declare const HEADLESS_GO_USAGE = "Usage: yagni go --headless --ticket-file <path> [--plan-file <path>] [--memo-file <path>] [--run-id <id>] [--cwd <path>] [--json]";
55
+ /**
56
+ * Exit codes. `verified` is deliberately narrow: only a `clean` stop means the
57
+ * run produced a reviewed candidate, so a round_cap / no_changes / failed run
58
+ * can never read as success to a script, a smoke gate, or CI.
59
+ */
60
+ export declare const HEADLESS_GO_EXIT: {
61
+ readonly verified: 0;
62
+ readonly unverified: 1;
63
+ readonly usage: 2;
64
+ };
65
+ /** The parsed flag surface (pure; no fs, no env). */
66
+ export interface HeadlessGoArgs {
67
+ headless: boolean;
68
+ json: boolean;
69
+ ticketFile?: string;
70
+ planFile?: string;
71
+ memoFile?: string;
72
+ runId?: string;
73
+ cwd?: string;
74
+ /** Any `--flag`-shaped token we do not know: refused, never folded in. */
75
+ unknownFlags: string[];
76
+ /** Bare tokens: the ticket is a FILE here, so a stray positional is refused. */
77
+ positionals: string[];
78
+ /**
79
+ * Value flags whose value was missing, empty, or another flag: refused, so
80
+ * `--ticket-file --json` fails naming the real problem instead of trying to
81
+ * read a file called `--json` with JSON mode silently off.
82
+ */
83
+ missingValues: string[];
84
+ }
85
+ /** One row of the result's per-stage usage table. */
86
+ export interface HeadlessStageRow {
87
+ stage: StageId;
88
+ /** The tier the stage actually ran on (capped when `YAGNI_GO_TIER_CAP` is set). */
89
+ tier?: ModelTier;
90
+ /** Present on review rows: which review round produced them. */
91
+ round?: number;
92
+ usage: StageUsage;
93
+ }
94
+ /** The final result object written to stdout and returned to the caller. */
95
+ export interface HeadlessGoResult {
96
+ /** True ONLY for a verified candidate (a `clean` stop) — mirrors exit 0. */
97
+ ok: boolean;
98
+ stopReason: StopReason;
99
+ rounds: number;
100
+ findings: number;
101
+ blocking: number;
102
+ stages: HeadlessStageRow[];
103
+ runId?: string;
104
+ /** The resolved `YAGNI_GO_TIER_CAP` ceiling; absent when the run is uncapped. */
105
+ tierCap?: ModelTier;
106
+ /**
107
+ * True when a plan or memo was injected: the pipeline entered at implement on
108
+ * the approved plan and FINISH did not run, so `commitSha` / `prUrl` are never
109
+ * reported (the mission's prHandoff delivers the candidate). Absent otherwise.
110
+ */
111
+ missionMode?: true;
112
+ commitSha?: string;
113
+ prUrl?: string;
114
+ /** Honest note when the deterministic verify gate gave no verdict. */
115
+ verifyNote?: string;
116
+ verifyCommand?: string;
117
+ }
118
+ export interface HeadlessGoOutcome {
119
+ exitCode: number;
120
+ result?: HeadlessGoResult;
121
+ /** Set when the run ended on a thrown error or an argument problem. */
122
+ error?: string;
123
+ }
124
+ /** Injectable seams: no fs, no stdout, no pipeline in a unit test. */
125
+ export interface HeadlessGoDeps {
126
+ /** Repo to run in. `--cwd` wins; defaults to `process.cwd()`. */
127
+ cwd?: string;
128
+ /** Environment the tier cap is read from. Defaults to `process.env`. */
129
+ env?: NodeJS.ProcessEnv;
130
+ /** Environment for spawned stage children. Defaults to `env`. */
131
+ childEnv?: NodeJS.ProcessEnv;
132
+ runPipeline?: typeof defaultRunPipeline;
133
+ readFile?: (path: string) => string;
134
+ /** stdout sink; one call per NDJSON line, newline added by the caller. */
135
+ write?: (line: string) => void;
136
+ /** stderr sink for human-readable problems. */
137
+ writeErr?: (line: string) => void;
138
+ signal?: AbortSignal;
139
+ budget?: RunBudget;
140
+ /** Stage-list override (the blind eval lane's grounding-stripped copy). */
141
+ stages?: PipelineStage[];
142
+ /** False runs the blind eval lane; defaults to grounded like every real run. */
143
+ grounded?: boolean;
144
+ /** Additive tap on every child event (the eval lane's retrieval telemetry). */
145
+ onEvent?: (ev: JsonEvent, tag: StageTag) => void;
146
+ /** Additive tap on the pipeline's own log events. */
147
+ logger?: (event: string, data?: unknown) => void;
148
+ }
149
+ /**
150
+ * Parse the argv remainder after `go`. Accepts both `--flag value` and
151
+ * `--flag=value`; unknown flags and stray positionals are collected rather than
152
+ * silently absorbed, so a typo fails loudly instead of running the wrong thing.
153
+ */
154
+ export declare function parseHeadlessGoArgs(argv: string[]): HeadlessGoArgs;
155
+ /** The argument problem, or undefined when the invocation is usable. */
156
+ export declare function validateHeadlessGoArgs(args: HeadlessGoArgs): string | undefined;
157
+ /**
158
+ * Run the pipeline headlessly. Never throws: every failure resolves to an exit
159
+ * code and an honest final line, so the sandbox harness and the smoke gate can
160
+ * read one contract.
161
+ */
162
+ export declare function runHeadlessGo(argv: string[], deps?: HeadlessGoDeps): Promise<HeadlessGoOutcome>;
163
+ //# sourceMappingURL=headlessGo.d.ts.map