@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
@@ -28,7 +28,8 @@
28
28
  */
29
29
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
30
30
  import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
31
- import { type BlessStore } from "./bless.js";
31
+ import type { HookRunner } from "../hooks.js";
32
+ import { type BlessStore } from "../bless.js";
32
33
  import { type ExecPolicy } from "./execPolicy.js";
33
34
  import { type GuardianError, type GuardianRiskLevel } from "./guardian.js";
34
35
  export type PermissionMode = "auto" | "plan" | "review";
@@ -179,6 +180,12 @@ export interface RegisterPermissionDeps {
179
180
  * Fail-soft; never blocks.
180
181
  */
181
182
  onGuardianEvent?: (event: GuardianGateEvent) => void;
183
+ /**
184
+ * User-configurable lifecycle hooks (YAG-506). When present, PreToolUse
185
+ * hooks run before decideGate and can short-circuit (allow/deny/ask),
186
+ * and PermissionRequest hooks run before the confirm dialog.
187
+ */
188
+ hookRunner?: HookRunner;
182
189
  }
183
190
  /** The customType tag on injected mode-context messages (filterable later). */
184
191
  export declare const MODE_CONTEXT_TYPE = "yagni-mode-context";
@@ -202,4 +209,4 @@ export declare const filterStalePlanContext: typeof filterStaleModeContext;
202
209
  * auto, so absent any /mode this is a no-op over today's behavior.
203
210
  */
204
211
  export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
205
- //# sourceMappingURL=permission.d.ts.map
212
+ //# sourceMappingURL=gate.d.ts.map
@@ -27,9 +27,9 @@
27
27
  * the context so the model doesn't keep believing it is restricted.
28
28
  */
29
29
  import { describePrefix, matchesGrant, validateGrant, } from "./approvedPrefixes.js";
30
- import { makeBlessStore as defaultMakeBlessStore } from "./bless.js";
30
+ import { makeBlessStore as defaultMakeBlessStore } from "../bless.js";
31
31
  import { classifyCommand, DEFAULT_EXEC_POLICY } from "./execPolicy.js";
32
- import { isDebug } from "./diagnostics.js";
32
+ import { isDebug } from "../diagnostics.js";
33
33
  import { buildDiagnosticEvent, checkCircuitBreaker, DEFAULT_GUARDIAN_LIMITS, } from "./guardian.js";
34
34
  export function createModeHolder(initial = "auto") {
35
35
  let current = initial;
@@ -296,6 +296,7 @@ export function registerPermissionGate(pi, deps = {}) {
296
296
  const basePolicy = deps.policy ?? DEFAULT_PERMISSION_POLICY;
297
297
  let mode = deps.mode ?? "auto";
298
298
  const makeStore = deps.makeBlessStore ?? defaultMakeBlessStore;
299
+ const hookRunner = deps.hookRunner;
299
300
  deps.modeHolder?.onSet((m) => {
300
301
  if (m !== mode)
301
302
  approvedCommands.clear();
@@ -422,7 +423,48 @@ export function registerPermissionGate(pi, deps = {}) {
422
423
  const modeAtEntry = mode;
423
424
  try {
424
425
  const input = event.input ?? {};
425
- const decision = decideGate(event.toolName, input, modeAtEntry, effectivePolicy);
426
+ // YAG-506: PreToolUse hooks run BEFORE decideGate. They can short-circuit
427
+ // (allow/deny/ask) or fall through to the normal gate logic. The result
428
+ // is cached in preToolUseResult so the "ask" check below does NOT
429
+ // re-invoke the hook (hooks have side effects — notifications etc.).
430
+ let preToolUseResult;
431
+ if (hookRunner) {
432
+ const cwd = ctx?.cwd ?? ".";
433
+ try {
434
+ preToolUseResult = await hookRunner.preToolUse(event.toolName, input, cwd, ctx?.isProjectTrusted()) ?? undefined;
435
+ if (preToolUseResult) {
436
+ if (preToolUseResult.decision === "deny") {
437
+ return { block: true, reason: preToolUseResult.reason };
438
+ }
439
+ if (preToolUseResult.decision === "allow") {
440
+ // Allow bypasses Guardian/confirm, but the exec policy's forbidden
441
+ // band still runs as a hard safety floor (deliberate deviation
442
+ // from Claude Code: we don't let a hook auto-allow a forbidden cmd).
443
+ if (event.toolName === "bash") {
444
+ const cmdRaw = input.command;
445
+ const command = typeof cmdRaw === "string" ? cmdRaw.trim() : "";
446
+ if (command) {
447
+ const execPolicy = effectivePolicy.execPolicy ?? DEFAULT_EXEC_POLICY;
448
+ const classification = classifyCommand(command, execPolicy);
449
+ if (classification.decision === "forbidden") {
450
+ return {
451
+ block: true,
452
+ reason: `${classification.justification}. Do not attempt the same outcome via a workaround or indirect execution — use a materially safer alternative, or ask the user.`,
453
+ };
454
+ }
455
+ }
456
+ }
457
+ return {};
458
+ }
459
+ // "ask" → force confirm by overriding the gate decision
460
+ // Falls through to decision.confirm logic below
461
+ }
462
+ }
463
+ catch {
464
+ // Fail-soft: a hook error never blocks or allows; fall through to gate
465
+ }
466
+ }
467
+ let decision = decideGate(event.toolName, input, modeAtEntry, effectivePolicy);
426
468
  if (decision.block)
427
469
  return { block: true, reason: decision.reason };
428
470
  // Prompt band (YAG-510 order): grants → exact-command cache → cap/
@@ -587,6 +629,41 @@ export function registerPermissionGate(pi, deps = {}) {
587
629
  reason: `Guardian needs user approval: ${verdict.rationale} No UI available — the command was held. Find a safer alternative or leave this step for the user.`,
588
630
  };
589
631
  }
632
+ // YAG-506: PermissionRequest hooks fire before the confirm dialog.
633
+ // Only when a UI is present (headless path already failed closed above).
634
+ if (hookRunner && ctx?.hasUI) {
635
+ try {
636
+ const hookResult = await hookRunner.permissionRequest(event.toolName, input, cwd, ctx?.isProjectTrusted());
637
+ if (hookResult) {
638
+ if (hookResult.decision === "allow") {
639
+ rememberApproved(cwd, command);
640
+ emitGateEvent({
641
+ ...eventBase,
642
+ outcome: "ask_approved",
643
+ riskLevel: verdict.riskLevel,
644
+ rationale: verdict.rationale,
645
+ durationMs,
646
+ consulted: true,
647
+ });
648
+ return {};
649
+ }
650
+ if (hookResult.decision === "deny") {
651
+ emitGateEvent({
652
+ ...eventBase,
653
+ outcome: "ask_denied",
654
+ riskLevel: verdict.riskLevel,
655
+ rationale: verdict.rationale,
656
+ durationMs,
657
+ consulted: true,
658
+ });
659
+ return { block: true, reason: hookResult.reason };
660
+ }
661
+ }
662
+ }
663
+ catch {
664
+ // Fail-soft: hook error → dialog proceeds normally
665
+ }
666
+ }
590
667
  // Offer "don't ask again" only when the grant would actually
591
668
  // cover this command (grant-time validation).
592
669
  const grantCandidate = validateGrant(command, effectivePolicy.execPolicy ?? DEFAULT_EXEC_POLICY, resolveRepoKeyFor(cwd));
@@ -725,7 +802,29 @@ export function registerPermissionGate(pi, deps = {}) {
725
802
  }
726
803
  // review mode with Guardian disabled/capped: fall through to confirm.
727
804
  }
805
+ // YAG-506: PreToolUse "ask" forces confirmation even in auto mode.
806
+ // Uses the cached result from the top of the handler — no re-invocation.
807
+ if (preToolUseResult?.decision === "ask") {
808
+ decision = { block: false, confirm: true };
809
+ }
728
810
  if (decision.confirm) {
811
+ // YAG-506: PermissionRequest hooks run before the confirm dialog.
812
+ if (hookRunner) {
813
+ try {
814
+ const cwd = ctx?.cwd ?? ".";
815
+ const hookResult = await hookRunner.permissionRequest(event.toolName, input, cwd, ctx?.isProjectTrusted());
816
+ if (hookResult) {
817
+ if (hookResult.decision === "allow")
818
+ return {};
819
+ if (hookResult.decision === "deny") {
820
+ return { block: true, reason: hookResult.reason };
821
+ }
822
+ }
823
+ }
824
+ catch {
825
+ // Fail-soft: hook error → dialog proceeds normally
826
+ }
827
+ }
729
828
  // Review mode needs a confirmation. With no dialog-capable UI (headless),
730
829
  // fail CLOSED: the user explicitly chose a stricter mode, so a write we
731
830
  // cannot get consent for is held rather than silently auto-applied (this
@@ -832,4 +931,4 @@ export function registerPermissionGate(pi, deps = {}) {
832
931
  },
833
932
  });
834
933
  }
835
- //# sourceMappingURL=permission.js.map
934
+ //# sourceMappingURL=gate.js.map
@@ -27,8 +27,8 @@
27
27
  * exoneration — so an ask-preferring model cannot disarm the breaker by
28
28
  * alternating deny/ask.
29
29
  */
30
- import { runStage as defaultRunStage } from "./pipeline/runner.js";
31
- import type { PipelineStage } from "./pipeline/types.js";
30
+ import { runStage as defaultRunStage } from "../pipeline/runner.js";
31
+ import type { PipelineStage } from "../pipeline/types.js";
32
32
  export type GuardianOutcome = "allow" | "ask" | "deny";
33
33
  export type GuardianRiskLevel = "low" | "medium" | "high" | "critical";
34
34
  export interface GuardianVerdict {
@@ -27,7 +27,7 @@
27
27
  * exoneration — so an ask-preferring model cannot disarm the breaker by
28
28
  * alternating deny/ask.
29
29
  */
30
- import { runStage as defaultRunStage } from "./pipeline/runner.js";
30
+ import { runStage as defaultRunStage } from "../pipeline/runner.js";
31
31
  export const DEFAULT_GUARDIAN_LIMITS = {
32
32
  maxReviews: 120,
33
33
  maxConsecutiveDenials: 3,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Permission domain barrel.
3
+ *
4
+ * This file is the *public* re-export surface ONLY. Internal modules import
5
+ * each other directly (gate.ts → ./execPolicy.js), never through this barrel,
6
+ * to keep the dependency graph grep-able and avoid circular imports.
7
+ *
8
+ * See AGENTS.md alongside this directory for the seam map and how to extend.
9
+ */
10
+ export * from "./execPolicy.js";
11
+ export * from "./guardian.js";
12
+ export * from "./approvedPrefixes.js";
13
+ export * from "./gate.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Permission domain barrel.
3
+ *
4
+ * This file is the *public* re-export surface ONLY. Internal modules import
5
+ * each other directly (gate.ts → ./execPolicy.js), never through this barrel,
6
+ * to keep the dependency graph grep-able and avoid circular imports.
7
+ *
8
+ * See AGENTS.md alongside this directory for the seam map and how to extend.
9
+ */
10
+ export * from "./execPolicy.js";
11
+ export * from "./guardian.js";
12
+ export * from "./approvedPrefixes.js";
13
+ export * from "./gate.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Package-manager and dev-loop auto-allow policy (YAG-561).
3
+ *
4
+ * Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
5
+ * package-manager / dev-loop band, while execPolicy.ts owns the core classifier
6
+ * (rule matching, construct floor, danger scan) and wires these helpers in.
7
+ *
8
+ * Two independent mechanisms live here:
9
+ *
10
+ * 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
11
+ * match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
12
+ * "which workspace" options (never arbitrary flags), because an option we
13
+ * do NOT recognize must degrade to `prompt`, never to `allow`.
14
+ *
15
+ * 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
16
+ * `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
17
+ * tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
18
+ * forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
19
+ *
20
+ * The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
21
+ * - dev-loop binaries reachable through a forwarder (and directly):
22
+ * tsc (requires --noEmit), tsx (requires --test), vitest, jest;
23
+ * - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
24
+ *
25
+ * Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
26
+ * `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
27
+ * and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
28
+ * prompt band and go to the Guardian.
29
+ *
30
+ * See AGENTS.md alongside this directory for the "add a new spelling" recipe.
31
+ */
32
+ import type { PrefixRule } from "./execPolicy.js";
33
+ /**
34
+ * Strip value-bearing "which workspace" options so they don't break prefix
35
+ * matching. Matching-only: the returned tokens are used to CLASSIFY, never to
36
+ * run. Only the handlers below are stripped; anything unrecognized falls
37
+ * through untouched and degrades to `prompt` (fail closed).
38
+ *
39
+ * pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
40
+ * npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
41
+ * yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
42
+ */
43
+ export declare function normalizePackageManagerTokens(tokens: string[]): string[];
44
+ /**
45
+ * If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
46
+ * return the index at which the forwarded command's argv begins. Return null
47
+ * otherwise. Forwarders exercise `require`d escape hatch: they match only when
48
+ * the manager word is a bare, path-unprefixed, unwrapped token (callers already
49
+ * forced `neverAllow` for wrappers/paths before this runs).
50
+ */
51
+ export declare function forwarderTailStart(tokens: string[]): number | null;
52
+ /** Human label for a forwarder, used in justification strings. */
53
+ export declare function forwarderLabel(tokens: string[]): string;
54
+ export declare const PACKAGE_MANAGER_ALLOW_RULES: PrefixRule[];
55
+ //# sourceMappingURL=packageManagerPolicy.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Package-manager and dev-loop auto-allow policy (YAG-561).
3
+ *
4
+ * Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
5
+ * package-manager / dev-loop band, while execPolicy.ts owns the core classifier
6
+ * (rule matching, construct floor, danger scan) and wires these helpers in.
7
+ *
8
+ * Two independent mechanisms live here:
9
+ *
10
+ * 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
11
+ * match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
12
+ * "which workspace" options (never arbitrary flags), because an option we
13
+ * do NOT recognize must degrade to `prompt`, never to `allow`.
14
+ *
15
+ * 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
16
+ * `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
17
+ * tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
18
+ * forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
19
+ *
20
+ * The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
21
+ * - dev-loop binaries reachable through a forwarder (and directly):
22
+ * tsc (requires --noEmit), tsx (requires --test), vitest, jest;
23
+ * - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
24
+ *
25
+ * Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
26
+ * `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
27
+ * and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
28
+ * prompt band and go to the Guardian.
29
+ *
30
+ * See AGENTS.md alongside this directory for the "add a new spelling" recipe.
31
+ */
32
+ // --- Workspace-selector normalization ---
33
+ /**
34
+ * Strip value-bearing "which workspace" options so they don't break prefix
35
+ * matching. Matching-only: the returned tokens are used to CLASSIFY, never to
36
+ * run. Only the handlers below are stripped; anything unrecognized falls
37
+ * through untouched and degrades to `prompt` (fail closed).
38
+ *
39
+ * pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
40
+ * npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
41
+ * yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
42
+ */
43
+ export function normalizePackageManagerTokens(tokens) {
44
+ const cmd = tokens[0];
45
+ if (cmd !== "pnpm" && cmd !== "npm" && cmd !== "yarn")
46
+ return tokens;
47
+ const out = [cmd];
48
+ let i = 1;
49
+ while (i < tokens.length) {
50
+ const t = tokens[i];
51
+ if (cmd === "pnpm") {
52
+ // `--filter pkg` / `-F pkg` → skip option + its value
53
+ if (t === "--filter" || t === "-F") {
54
+ i += 2;
55
+ continue;
56
+ }
57
+ // glued `--filter=pkg` / `-F=pkg` → skip the single token
58
+ if (t.startsWith("--filter=") || t.startsWith("-F=")) {
59
+ i += 1;
60
+ continue;
61
+ }
62
+ }
63
+ else if (cmd === "npm") {
64
+ // `--workspace pkg` / `-w pkg` → skip option + its value
65
+ if (t === "--workspace" || t === "-w") {
66
+ i += 2;
67
+ continue;
68
+ }
69
+ if (t.startsWith("--workspace=") || t.startsWith("-w=")) {
70
+ i += 1;
71
+ continue;
72
+ }
73
+ }
74
+ // First non-selector token (yarn has none we handle) — stop stripping.
75
+ break;
76
+ }
77
+ out.push(...tokens.slice(i));
78
+ return out;
79
+ }
80
+ // --- Exec/dlx forwarders ---
81
+ /**
82
+ * If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
83
+ * return the index at which the forwarded command's argv begins. Return null
84
+ * otherwise. Forwarders exercise `require`d escape hatch: they match only when
85
+ * the manager word is a bare, path-unprefixed, unwrapped token (callers already
86
+ * forced `neverAllow` for wrappers/paths before this runs).
87
+ */
88
+ export function forwarderTailStart(tokens) {
89
+ const cmd = tokens[0];
90
+ if (cmd === "xargs" || cmd === "npx")
91
+ return 1;
92
+ if (tokens.length < 2)
93
+ return null;
94
+ const sub = tokens[1];
95
+ if (sub === "exec" && (cmd === "pnpm" || cmd === "npm" || cmd === "yarn"))
96
+ return 2;
97
+ if (sub === "dlx" && (cmd === "pnpm" || cmd === "yarn"))
98
+ return 2;
99
+ return null;
100
+ }
101
+ /** Human label for a forwarder, used in justification strings. */
102
+ export function forwarderLabel(tokens) {
103
+ const cmd = tokens[0];
104
+ if (cmd === "npx" || cmd === "xargs")
105
+ return cmd;
106
+ return tokens.slice(0, 2).join(" ");
107
+ }
108
+ // --- Allow rules ---
109
+ /**
110
+ * The auto-allow rules for the package-manager / dev-loop band. Appended to
111
+ * DEFAULT_EXEC_POLICY.rules by execPolicy.ts (first-match-wins ordering puts
112
+ * these specific rules ahead of the `npx`/`run`/`exec` prompt fallbacks).
113
+ *
114
+ * Safety invariants for every entry:
115
+ * - tsc only allows with --noEmit (bare `tsc` would emit .js/.d.ts).
116
+ * - tsx only allows with --test (bare `tsx file.ts` executes arbitrary TS).
117
+ * - vitest / jest are pure test runners (no mutating flag to fence).
118
+ * - script namespaces are read-only-by-convention test/build/lint and are
119
+ * the ONLY `run` targets allowed; everything else (`run deploy`) is prompt.
120
+ */
121
+ // The bare script name AND its `name:*` namespace (e.g. both `test` and
122
+ // `test:backend`) — declared once so each manager rule reuses the same set.
123
+ const SCRIPT_NAMESPACES = ["test", "test:*", "build", "build:*", "lint", "lint:*"];
124
+ export const PACKAGE_MANAGER_ALLOW_RULES = [
125
+ // dev-loop binaries (reachable directly or through a forwarder)
126
+ {
127
+ pattern: ["tsc"],
128
+ flagsAnywhere: ["--noEmit"],
129
+ decision: "allow",
130
+ justification: "typecheck only (--noEmit writes no files)",
131
+ },
132
+ {
133
+ pattern: ["tsx"],
134
+ flagsAnywhere: ["--test"],
135
+ decision: "allow",
136
+ justification: "run tests via tsx (--test only, no script execution)",
137
+ },
138
+ { pattern: ["vitest"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
139
+ { pattern: ["vitest", "run"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
140
+ { pattern: ["jest"], decision: "allow", justification: "run jest tests (routine dev-loop operation)" },
141
+ // script namespaces — direct form (pnpm/yarn support `mgr <script>`) and
142
+ // universal `run <script>` form. npm direct form is omitted: npm requires
143
+ // `run` for colon-namespaced scripts.
144
+ {
145
+ pattern: ["pnpm", SCRIPT_NAMESPACES],
146
+ decision: "allow",
147
+ justification: "run a test/build/lint script (routine dev-loop operation)",
148
+ },
149
+ {
150
+ pattern: ["pnpm", "run", SCRIPT_NAMESPACES],
151
+ decision: "allow",
152
+ justification: "run a test/build/lint script (routine dev-loop operation)",
153
+ },
154
+ {
155
+ pattern: ["yarn", SCRIPT_NAMESPACES],
156
+ decision: "allow",
157
+ justification: "run a test/build/lint script (routine dev-loop operation)",
158
+ },
159
+ {
160
+ pattern: ["yarn", "run", SCRIPT_NAMESPACES],
161
+ decision: "allow",
162
+ justification: "run a test/build/lint script (routine dev-loop operation)",
163
+ },
164
+ {
165
+ pattern: ["npm", "run", SCRIPT_NAMESPACES],
166
+ decision: "allow",
167
+ justification: "run a test/build/lint script (routine dev-loop operation)",
168
+ },
169
+ ];
170
+ //# sourceMappingURL=packageManagerPolicy.js.map
@@ -92,9 +92,10 @@ export class ActivityFeed {
92
92
  }
93
93
  /** Fold a structured progress signal into stage transitions + the header tally. */
94
94
  applyProgress(p) {
95
- // A per-lens signal is a desktop concern: it must not steal the buffer from
96
- // the review stage that owns it, nor reset it mid fan-out.
97
- if (p.kind === "stage_start" && !p.lens && p.stageId !== this.bufferStageId) {
95
+ // A per-lens or per-workstream signal is a desktop concern: it must not steal
96
+ // the buffer from the stage that owns it, nor reset it mid fan-out. The
97
+ // implement diamond's builders get exactly the review lenses' treatment.
98
+ if (p.kind === "stage_start" && !p.lens && !p.workstream && p.stageId !== this.bufferStageId) {
98
99
  this.bufferStageId = p.stageId;
99
100
  this.actions = [];
100
101
  }
@@ -129,6 +130,10 @@ export class ActivityFeed {
129
130
  headerParts.push(elapsed);
130
131
  const header = theme.bold(clipRow(headerParts.join(" · "), ROW_MAX));
131
132
  const lines = [header];
133
+ // The implement diamond collapses to ONE row plus a compact summary, the same
134
+ // way the review fan-out collapses to one review row: per-child rows are the
135
+ // desktop's business and would break the 10-line cap here.
136
+ const fan = this.run.fanoutRow();
132
137
  // Only the lens-less stage rows: the review fan-out's per-lens children are
133
138
  // the desktop's business, and surfacing them here would break the 10-line cap.
134
139
  for (const s of this.run.stageAgents()) {
@@ -138,8 +143,17 @@ export class ActivityFeed {
138
143
  if (s.id === "finish" && s.status === "pending")
139
144
  continue;
140
145
  const glyph = glyphForStatus(s.status, spinnerFrame);
141
- const label = s.stageId.padEnd(9);
142
- const row = clipRow(`${glyph} ${label}${s.summary}`.trimEnd(), ROW_MAX);
146
+ // Padded past the longest stage name ("implement", 9) so every row keeps a
147
+ // separator between the label and its note; at 9 the implement row ran its
148
+ // name straight into its own summary.
149
+ const label = s.stageId.padEnd(10);
150
+ // While the fan is live the implement row's note IS the fan summary; once
151
+ // the stage settles its own narration takes the row back (and the summary
152
+ // stands in when there is no narration to show).
153
+ const fanNote = s.stageId === "implement" && fan?.mode === "fan" && (s.status === "active" || !s.summary)
154
+ ? `fanned ${fan.total} ways · ${fan.done}/${fan.total} done`
155
+ : undefined;
156
+ const row = clipRow(`${glyph} ${label}${fanNote ?? s.summary}`.trimEnd(), ROW_MAX);
143
157
  lines.push(theme.fg(themeColorForStatus[s.status], row));
144
158
  // The ring buffer renders under its OWNING stage (see RunState), so a
145
159
  // just-finished stage's resolved actions stay briefly visible beneath its ✔
@@ -0,0 +1,99 @@
1
+ /**
2
+ * PURE checker-side helpers for the implement diamond (spec decisions 5-7).
3
+ *
4
+ * The checker itself is deterministic and lives in `verify.ts` (the per-workstream
5
+ * scoped typecheck plus the one full `makeRunVerify` on the merged tree). What is
6
+ * left is the reading of its verdict, and that is all pure text math:
7
+ *
8
+ * - {@link checkerFindings} unions the scoped + full findings into one deduped list.
9
+ * - {@link composeSynthesizerInput} turns the fan handoff plus the checker's
10
+ * verdict into the synthesizer's `{previous}`.
11
+ * - {@link attributeFindings} routes each finding to the workstream whose claims
12
+ * cover its file; anything unattributable is the synthesizer's (or, on the
13
+ * single-writer path, the one builder's).
14
+ * - {@link parseOpenFindings} reads the synthesizer's own "## Open findings"
15
+ * section, because decision 7 lets the summary report defects the deterministic
16
+ * checker cannot see.
17
+ * - {@link composeResidue} names what is still open when the fix cap is reached,
18
+ * so `reviewInput` hands the review loop the truth rather than a clean-looking
19
+ * summary.
20
+ *
21
+ * No I/O, no model calls: the orchestrator owns the children, this module owns the
22
+ * reading, exactly the split `findings.ts` and `fanout.ts` already use.
23
+ */
24
+ import { type PartitionWorkstream } from "./fanout.js";
25
+ import type { Finding } from "./types.js";
26
+ import type { VerifyOutcome, WorkstreamCheckResult } from "./verify.js";
27
+ /** Everything the deterministic checker produced for one pass over the tree. */
28
+ export interface CheckerReport {
29
+ /** Per-workstream scoped typecheck verdicts (empty on the single-writer path). */
30
+ scoped: WorkstreamCheckResult[];
31
+ /** The one full verify on the merged tree; null when it could not be consulted. */
32
+ verify: VerifyOutcome | null;
33
+ }
34
+ /** Findings routed to one workstream's builder for a fix turn. */
35
+ export interface FindingAssignment {
36
+ workstream: PartitionWorkstream;
37
+ findings: Finding[];
38
+ }
39
+ /** How the fix loop splits a checker verdict across the children that can fix it. */
40
+ export interface FindingAttribution {
41
+ assigned: FindingAssignment[];
42
+ /** Findings no workstream's claims cover: the synthesizer's seam work. */
43
+ unassigned: Finding[];
44
+ }
45
+ /** Union findings in first-seen order, deduped by file + line + message. */
46
+ export declare function mergeFindings(...groups: Finding[][]): Finding[];
47
+ /** Every defect this checker pass found: the scoped typechecks plus the full verify. */
48
+ export declare function checkerFindings(report: CheckerReport): Finding[];
49
+ /** One finding as a line a builder can act on (mirrors the review loop's handoff shape). */
50
+ export declare function renderFindings(findings: Finding[]): string;
51
+ /**
52
+ * The synthesizer's `{previous}`: the fan's own handoff (which already names each
53
+ * workstream, its claims, and any out-of-claim edits) plus the checker's verdict,
54
+ * with a broken workstream attributed BY NAME so the reconciler knows where to look.
55
+ */
56
+ export declare function composeSynthesizerInput(handoff: string, report: CheckerReport): string;
57
+ /**
58
+ * Route findings to the builders that can fix them: a finding whose file sits under
59
+ * a workstream's claims goes to that workstream (at its original tier), and anything
60
+ * else - no file, a file outside every claim, a seam between two workstreams - is
61
+ * unattributable and belongs to the synthesizer. With no workstreams (the
62
+ * single-writer path) everything is unassigned, which is exactly right: there is one
63
+ * builder and it owns the whole tree.
64
+ */
65
+ export declare function attributeFindings(findings: Finding[], workstreams: PartitionWorkstream[]): FindingAttribution;
66
+ /**
67
+ * The synthesizer's own "## Open findings" section (its persona output contract),
68
+ * as findings the fix loop can act on. "none" is a real answer and returns nothing;
69
+ * an absent section returns nothing too, because a summary that never claimed a
70
+ * defect is not evidence of one. Deliberately forgiving about the prose: what the
71
+ * loop needs is the line and, where the synthesizer named one, the file.
72
+ */
73
+ export declare function parseOpenFindings(summary: string): Finding[];
74
+ /**
75
+ * The same "## Open findings" section {@link parseOpenFindings} reads, REMOVED
76
+ * from a summary (heading and body, up to the next heading of any depth).
77
+ *
78
+ * A fix turn that only re-engaged builders never re-runs the synthesizer, so its
79
+ * summary stays the handoff verbatim. Once those defects are answered and the
80
+ * re-check is clean, leaving the section in place would hand the review loop a
81
+ * document that names open defects in one paragraph and calls the final check
82
+ * clean in the next. Everything else the synthesizer wrote is untouched: this
83
+ * drops only the section the fix loop has since resolved.
84
+ */
85
+ export declare function stripOpenFindings(summary: string): string;
86
+ /**
87
+ * What `reviewInput` says when the fix loop ran and then came back clean: the
88
+ * reviewers should know the candidate was repaired inside the implement stage, not
89
+ * that it was right first time. Absent when no fix turn ran, which keeps a
90
+ * clean-on-the-first-check run byte-identical to today's handoff.
91
+ */
92
+ export declare function composeFixNote(turns: number): string;
93
+ /**
94
+ * What `reviewInput` says when the fix cap is reached with defects still open
95
+ * (spec decision 7): named, not hidden, so the review loop picks them up knowing
96
+ * the implement stage already spent its turns on them.
97
+ */
98
+ export declare function composeResidue(findings: Finding[], turns: number): string;
99
+ //# sourceMappingURL=checker.d.ts.map