@cluesmith/codev 3.3.1 → 3.3.2

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 (118) hide show
  1. package/dashboard-dist/assets/index-D3XaKpZW.js.map +1 -1
  2. package/dist/agent-farm/cli.d.ts.map +1 -1
  3. package/dist/agent-farm/cli.js +52 -1
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/reset/constants.d.ts +118 -0
  6. package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
  7. package/dist/agent-farm/commands/reset/constants.js +118 -0
  8. package/dist/agent-farm/commands/reset/constants.js.map +1 -1
  9. package/dist/agent-farm/commands/reset/context.d.ts +18 -0
  10. package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
  11. package/dist/agent-farm/commands/reset/context.js +41 -0
  12. package/dist/agent-farm/commands/reset/context.js.map +1 -1
  13. package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/reset/receipt.js +16 -5
  15. package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
  16. package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
  17. package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/reset/reorient.js +8 -1
  19. package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
  20. package/dist/agent-farm/commands/reset/self.d.ts +335 -0
  21. package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
  22. package/dist/agent-farm/commands/reset/self.js +636 -0
  23. package/dist/agent-farm/commands/reset/self.js.map +1 -0
  24. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  25. package/dist/agent-farm/commands/reset.js +23 -18
  26. package/dist/agent-farm/commands/reset.js.map +1 -1
  27. package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
  28. package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
  29. package/dist/agent-farm/commands/self-refresh.js +506 -0
  30. package/dist/agent-farm/commands/self-refresh.js.map +1 -0
  31. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/send.js +27 -2
  33. package/dist/agent-farm/commands/send.js.map +1 -1
  34. package/dist/agent-farm/commands/spawn.d.ts +7 -0
  35. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  36. package/dist/agent-farm/commands/spawn.js +13 -1
  37. package/dist/agent-farm/commands/spawn.js.map +1 -1
  38. package/dist/agent-farm/lib/tower-client.d.ts +12 -0
  39. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  40. package/dist/agent-farm/lib/tower-client.js +12 -0
  41. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  42. package/dist/agent-farm/servers/mailbox-delivery.d.ts +141 -13
  43. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  44. package/dist/agent-farm/servers/mailbox-delivery.js +172 -16
  45. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  46. package/dist/agent-farm/servers/mailbox-wiring.d.ts +23 -1
  47. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  48. package/dist/agent-farm/servers/mailbox-wiring.js +91 -31
  49. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  50. package/dist/agent-farm/servers/message-write.d.ts +58 -18
  51. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  52. package/dist/agent-farm/servers/message-write.js +73 -28
  53. package/dist/agent-farm/servers/message-write.js.map +1 -1
  54. package/dist/agent-farm/servers/render-gate.d.ts +13 -0
  55. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  56. package/dist/agent-farm/servers/render-gate.js +21 -0
  57. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  58. package/dist/agent-farm/servers/session-submit.d.ts +220 -27
  59. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  60. package/dist/agent-farm/servers/session-submit.js +333 -42
  61. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  62. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  63. package/dist/agent-farm/servers/tower-routes.js +114 -25
  64. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  65. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  66. package/dist/agent-farm/servers/tower-tunnel.js +14 -0
  67. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  68. package/dist/agent-farm/types.d.ts +36 -1
  69. package/dist/agent-farm/types.d.ts.map +1 -1
  70. package/dist/agent-farm/utils/message-format.d.ts +60 -4
  71. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  72. package/dist/agent-farm/utils/message-format.js +115 -22
  73. package/dist/agent-farm/utils/message-format.js.map +1 -1
  74. package/dist/agent-farm/utils/server-utils.d.ts +8 -2
  75. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  76. package/dist/agent-farm/utils/server-utils.js +19 -2
  77. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  78. package/dist/commands/porch/context-refresh.d.ts +181 -0
  79. package/dist/commands/porch/context-refresh.d.ts.map +1 -0
  80. package/dist/commands/porch/context-refresh.js +270 -0
  81. package/dist/commands/porch/context-refresh.js.map +1 -0
  82. package/dist/commands/porch/index.d.ts +6 -0
  83. package/dist/commands/porch/index.d.ts.map +1 -1
  84. package/dist/commands/porch/index.js +46 -0
  85. package/dist/commands/porch/index.js.map +1 -1
  86. package/dist/commands/porch/next.d.ts +0 -9
  87. package/dist/commands/porch/next.d.ts.map +1 -1
  88. package/dist/commands/porch/next.js +133 -4
  89. package/dist/commands/porch/next.js.map +1 -1
  90. package/dist/commands/porch/protocol.d.ts.map +1 -1
  91. package/dist/commands/porch/protocol.js +102 -0
  92. package/dist/commands/porch/protocol.js.map +1 -1
  93. package/dist/commands/porch/types.d.ts +61 -0
  94. package/dist/commands/porch/types.d.ts.map +1 -1
  95. package/dist/lib/self-refresh-invocation.d.ts +60 -0
  96. package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
  97. package/dist/lib/self-refresh-invocation.js +64 -0
  98. package/dist/lib/self-refresh-invocation.js.map +1 -0
  99. package/dist/lib/test-env.d.ts +50 -0
  100. package/dist/lib/test-env.d.ts.map +1 -1
  101. package/dist/lib/test-env.js +76 -0
  102. package/dist/lib/test-env.js.map +1 -1
  103. package/package.json +4 -4
  104. package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
  105. package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
  106. package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
  107. package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
  108. package/skeleton/protocol-schema.json +17 -0
  109. package/skeleton/protocols/air/protocol.json +1 -1
  110. package/skeleton/protocols/aspir/protocol.json +5 -1
  111. package/skeleton/protocols/bugfix/protocol.json +1 -1
  112. package/skeleton/protocols/experiment/protocol.json +1 -1
  113. package/skeleton/protocols/maintain/protocol.json +1 -1
  114. package/skeleton/protocols/pir/protocol.json +1 -1
  115. package/skeleton/protocols/protocol-schema.json +17 -0
  116. package/skeleton/protocols/research/protocol.json +1 -1
  117. package/skeleton/protocols/spike/protocol.json +1 -1
  118. package/skeleton/protocols/spir/protocol.json +5 -1
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Context-refresh boundary resolution and task emission (Spec 1470).
3
+ *
4
+ * ## What a boundary is, and why it is recorded rather than inferred
5
+ *
6
+ * A boundary is a moment porch already transitions the state machine, at which a
7
+ * builder's context can be discarded without losing anything: the spec, the plan,
8
+ * `status.yaml`, the thread narrative and git carry the durable state, so a
9
+ * refreshed builder re-orients from disk rather than from memory.
10
+ *
11
+ * The refresh is DESTRUCTIVE — it ends in `/clear`, which has no undo — so it must
12
+ * fire at most once per boundary. Porch transitions can loop: issue #1408 saw
13
+ * verify-approval reset every plan phase back to `pending`, which would replay
14
+ * every plan-phase advance. A side effect that wipes context cannot be guarded by
15
+ * inferring "we must already have done this" from phase or iteration; it has to be
16
+ * a RECORDED FACT. That is what `context_refreshes` in `status.yaml` is.
17
+ *
18
+ * ## Where at-most-once actually comes from
19
+ *
20
+ * Not from a lock or a guard, but from control flow. Each transition site mutates
21
+ * the phase fields AND appends the boundary record in ONE `writeStateAndCommit`,
22
+ * then returns the refresh task INSTEAD of recursing into `next()`. The next
23
+ * `porch next` reads a state where the boundary is already recorded, so
24
+ * {@link hasRefreshed} is true and the normal path runs. There is no window in
25
+ * which the record and the transition disagree, because they are the same write.
26
+ *
27
+ * ## Failure semantics: consumed means consumed
28
+ *
29
+ * The boundary is recorded when the task is EMITTED, not when the refresh
30
+ * succeeds. A refresh that fails, or that the builder never runs, is never
31
+ * retried. That is deliberate:
32
+ *
33
+ * - The builder-side command never writes `status.yaml` (only porch does), so
34
+ * there is no completion signal to model and no way for a failed refresh to
35
+ * corrupt protocol state.
36
+ * - A missed refresh costs context and nothing else. A repeated one costs a
37
+ * builder's working memory, over and over, with nobody watching.
38
+ *
39
+ * Asymmetric costs, so the design fails toward doing nothing.
40
+ */
41
+ import type { PorchTask, ProjectState, Protocol } from './types.js';
42
+ /**
43
+ * Identifier for entering a protocol phase.
44
+ *
45
+ * Derived from the transition rather than from a configured label, so the record
46
+ * in `status.yaml` cannot drift from the event that produced it.
47
+ */
48
+ export declare function enterBoundary(phaseId: string): string;
49
+ /** Identifier for advancing INTO a given plan phase. */
50
+ export declare function planPhaseBoundary(planPhaseId: string): string;
51
+ /**
52
+ * Is a phase-entry boundary declared for this protocol?
53
+ *
54
+ * Inspects the FIELDS, never the presence of the `context_refresh` object.
55
+ * `context_refresh: {}` is valid and declares nothing, so a truthiness check on
56
+ * the object would report every boundary as declared for a protocol that opted
57
+ * into none of them.
58
+ */
59
+ export declare function declaresEnter(protocol: Protocol, phaseId: string): boolean;
60
+ /** Is the plan-phase-advance boundary declared for this protocol? */
61
+ export declare function declaresPlanPhaseAdvance(protocol: Protocol): boolean;
62
+ /** Has this boundary already been refreshed for this project? */
63
+ export declare function hasRefreshed(state: ProjectState, boundary: string): boolean;
64
+ /**
65
+ * Record a boundary on the state object.
66
+ *
67
+ * Mutates rather than writing: the caller folds this into the SAME
68
+ * `writeStateAndCommit` as the transition itself, which is what makes the record
69
+ * and the transition atomic. A separate write here would open exactly the window
70
+ * this design closes.
71
+ */
72
+ export declare function recordRefresh(state: ProjectState, boundary: string, now: string): void;
73
+ /**
74
+ * Mark every recorded boundary as acknowledged, returning true if anything changed.
75
+ *
76
+ * ## Why an acknowledgment exists at all
77
+ *
78
+ * A refresh is unattended, and its worst failure is silent: the builder clears,
79
+ * the re-entry never arrives, and nothing ever says so. The plan's first design
80
+ * tried to DERIVE that from `updated_at` — but the normal task-emission path in
81
+ * `next()` writes no state, so `updated_at` stays pinned at the transition for
82
+ * the whole of a healthy build. A threshold long enough to avoid false positives
83
+ * would be far too long to catch the stall it exists for.
84
+ *
85
+ * So porch records the fact instead. This runs on the first `porch next` that
86
+ * reaches the NORMAL path after a boundary — which only happens if a builder came
87
+ * back and asked for work. A boundary recorded but never acknowledged therefore
88
+ * means exactly one thing: nobody returned.
89
+ *
90
+ * ## Why porch owns it
91
+ *
92
+ * The builder-side command is forbidden from writing `status.yaml`, and that
93
+ * prohibition is load-bearing — it is what makes a failed refresh incapable of
94
+ * corrupting protocol state. So the acknowledgment cannot come from the side that
95
+ * would most naturally report "I'm back"; it has to be inferred by the only
96
+ * writer, from the only evidence it has: the builder asked for its next task.
97
+ *
98
+ * Writes at most once per boundary, so this costs one extra state write per
99
+ * refresh rather than one per `porch next`.
100
+ *
101
+ * ## What this cannot distinguish, and why that is accepted
102
+ *
103
+ * Any normal-path pass acknowledges EVERY outstanding boundary. So a refresh
104
+ * that was refused before clearing — after which the builder simply runs `porch
105
+ * next` — reads as acknowledged, as does a stall a human recovered by hand.
106
+ *
107
+ * That is not a bug to fix here, because porch has exactly one piece of
108
+ * evidence: the builder asked for work. It cannot tell "cleared and came back"
109
+ * from "never cleared" from "was rescued", and inventing a distinction it cannot
110
+ * observe would be worse than admitting the limit. What the signal genuinely
111
+ * means is narrow and still useful: **no builder has asked for work since this
112
+ * boundary was recorded.**
113
+ */
114
+ export declare function acknowledgeRefreshes(state: ProjectState, now: string): boolean;
115
+ /**
116
+ * How long a refresh may legitimately take before silence means trouble.
117
+ *
118
+ * A refresh is not instant: the builder writes a save (minutes, for a real one),
119
+ * the re-entry is held by Tower for {@link DEFAULT_REENTRY_DELAY_SECONDS}, the
120
+ * mailbox waits for a clean prompt, and only then does the builder run `porch
121
+ * next`. Ten minutes is comfortably past all of that and still far sooner than a
122
+ * human would otherwise notice a builder sitting cleared and idle.
123
+ */
124
+ export declare const REFRESH_STALL_GRACE_MS: number;
125
+ /**
126
+ * Boundaries recorded but never acknowledged — the raw fact, no timing applied.
127
+ *
128
+ * Use this for HISTORY (what has and has not been acknowledged). For deciding
129
+ * whether to warn a human, use {@link stalledRefreshes}, which adds the grace
130
+ * period.
131
+ */
132
+ export declare function unacknowledgedRefreshes(state: ProjectState): Array<{
133
+ boundary: string;
134
+ at: string;
135
+ }>;
136
+ /**
137
+ * Unacknowledged boundaries old enough that silence is a problem.
138
+ *
139
+ * ## Why a grace period after all
140
+ *
141
+ * An earlier version of this warned on ANY unacknowledged boundary, with a
142
+ * comment arguing that time-based rules do not work here. That argument
143
+ * conflated two different things, and the result was a warning on every healthy
144
+ * refresh — a false positive during completely normal operation, every single
145
+ * time.
146
+ *
147
+ * What genuinely does not work is deriving the stall from `updated_at`, because
148
+ * that timestamp does not move during a healthy build. But the ACKNOWLEDGMENT
149
+ * does move, exactly when the builder returns. Given a reliable event, a grace
150
+ * period is simply the answer to "has enough time passed that silence is
151
+ * suspicious?" — which is the question, and which the plan asked for in the
152
+ * words "past a threshold".
153
+ *
154
+ * A signal that fires during normal operation is not a signal.
155
+ */
156
+ export declare function stalledRefreshes(state: ProjectState, now: number, graceMs?: number): Array<{
157
+ boundary: string;
158
+ at: string;
159
+ ageMs: number;
160
+ }>;
161
+ /**
162
+ * Should a refresh fire for `boundary`, and is it declared?
163
+ *
164
+ * Deliberately does NOT consider how much context the builder has used. The spec
165
+ * fixes always-fire-at-configured-boundaries over threshold triggering:
166
+ * thresholds would need harness introspection porch does not have, and would make
167
+ * the behavior non-deterministic and untestable. An unnecessary refresh at a
168
+ * clean boundary costs a re-read of artifacts the builder can already see.
169
+ */
170
+ export declare function shouldRefresh(state: ProjectState, declared: boolean, boundary: string): boolean;
171
+ /**
172
+ * The single task emitted at a boundary.
173
+ *
174
+ * Deliberately does NOT instruct `porch done`. A refresh is not a build: `porch
175
+ * done` would validate checks and advance the build state, which is wrong at a
176
+ * moment when nothing was built. The builder refreshes and then re-enters through
177
+ * `porch next`, which is also what the post-clear re-entry frame says — so both
178
+ * paths converge on the same next command whether or not the clear lands.
179
+ */
180
+ export declare function buildRefreshTask(boundary: string): PorchTask;
181
+ //# sourceMappingURL=context-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-refresh.d.ts","sourceRoot":"","sources":["../../../src/commands/porch/context-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EAAwB,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAM1F;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG1E;AAED,qEAAqE;AACrE,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEpE;AAMD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAItF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AAErD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,YAAY,GAClB,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAIzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,MAAM,EACX,OAAO,SAAyB,GAC/B,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAOxD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAET;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA0C5D"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Context-refresh boundary resolution and task emission (Spec 1470).
3
+ *
4
+ * ## What a boundary is, and why it is recorded rather than inferred
5
+ *
6
+ * A boundary is a moment porch already transitions the state machine, at which a
7
+ * builder's context can be discarded without losing anything: the spec, the plan,
8
+ * `status.yaml`, the thread narrative and git carry the durable state, so a
9
+ * refreshed builder re-orients from disk rather than from memory.
10
+ *
11
+ * The refresh is DESTRUCTIVE — it ends in `/clear`, which has no undo — so it must
12
+ * fire at most once per boundary. Porch transitions can loop: issue #1408 saw
13
+ * verify-approval reset every plan phase back to `pending`, which would replay
14
+ * every plan-phase advance. A side effect that wipes context cannot be guarded by
15
+ * inferring "we must already have done this" from phase or iteration; it has to be
16
+ * a RECORDED FACT. That is what `context_refreshes` in `status.yaml` is.
17
+ *
18
+ * ## Where at-most-once actually comes from
19
+ *
20
+ * Not from a lock or a guard, but from control flow. Each transition site mutates
21
+ * the phase fields AND appends the boundary record in ONE `writeStateAndCommit`,
22
+ * then returns the refresh task INSTEAD of recursing into `next()`. The next
23
+ * `porch next` reads a state where the boundary is already recorded, so
24
+ * {@link hasRefreshed} is true and the normal path runs. There is no window in
25
+ * which the record and the transition disagree, because they are the same write.
26
+ *
27
+ * ## Failure semantics: consumed means consumed
28
+ *
29
+ * The boundary is recorded when the task is EMITTED, not when the refresh
30
+ * succeeds. A refresh that fails, or that the builder never runs, is never
31
+ * retried. That is deliberate:
32
+ *
33
+ * - The builder-side command never writes `status.yaml` (only porch does), so
34
+ * there is no completion signal to model and no way for a failed refresh to
35
+ * corrupt protocol state.
36
+ * - A missed refresh costs context and nothing else. A repeated one costs a
37
+ * builder's working memory, over and over, with nobody watching.
38
+ *
39
+ * Asymmetric costs, so the design fails toward doing nothing.
40
+ */
41
+ import { selfRefreshInvocation } from '../../lib/self-refresh-invocation.js';
42
+ // ============================================================================
43
+ // Boundary identifiers
44
+ // ============================================================================
45
+ /**
46
+ * Identifier for entering a protocol phase.
47
+ *
48
+ * Derived from the transition rather than from a configured label, so the record
49
+ * in `status.yaml` cannot drift from the event that produced it.
50
+ */
51
+ export function enterBoundary(phaseId) {
52
+ return `enter:${phaseId}`;
53
+ }
54
+ /** Identifier for advancing INTO a given plan phase. */
55
+ export function planPhaseBoundary(planPhaseId) {
56
+ return `plan-phase:${planPhaseId}`;
57
+ }
58
+ // ============================================================================
59
+ // Declaration lookup
60
+ // ============================================================================
61
+ /**
62
+ * Is a phase-entry boundary declared for this protocol?
63
+ *
64
+ * Inspects the FIELDS, never the presence of the `context_refresh` object.
65
+ * `context_refresh: {}` is valid and declares nothing, so a truthiness check on
66
+ * the object would report every boundary as declared for a protocol that opted
67
+ * into none of them.
68
+ */
69
+ export function declaresEnter(protocol, phaseId) {
70
+ const config = protocol.context_refresh;
71
+ return Array.isArray(config?.on_enter) && config.on_enter.includes(phaseId);
72
+ }
73
+ /** Is the plan-phase-advance boundary declared for this protocol? */
74
+ export function declaresPlanPhaseAdvance(protocol) {
75
+ return protocol.context_refresh?.on_plan_phase_advance === true;
76
+ }
77
+ // ============================================================================
78
+ // The record
79
+ // ============================================================================
80
+ /** Has this boundary already been refreshed for this project? */
81
+ export function hasRefreshed(state, boundary) {
82
+ return (state.context_refreshes ?? []).some(r => r.boundary === boundary);
83
+ }
84
+ /**
85
+ * Record a boundary on the state object.
86
+ *
87
+ * Mutates rather than writing: the caller folds this into the SAME
88
+ * `writeStateAndCommit` as the transition itself, which is what makes the record
89
+ * and the transition atomic. A separate write here would open exactly the window
90
+ * this design closes.
91
+ */
92
+ export function recordRefresh(state, boundary, now) {
93
+ if (!state.context_refreshes)
94
+ state.context_refreshes = [];
95
+ if (hasRefreshed(state, boundary))
96
+ return;
97
+ state.context_refreshes.push({ boundary, at: now });
98
+ }
99
+ /**
100
+ * Mark every recorded boundary as acknowledged, returning true if anything changed.
101
+ *
102
+ * ## Why an acknowledgment exists at all
103
+ *
104
+ * A refresh is unattended, and its worst failure is silent: the builder clears,
105
+ * the re-entry never arrives, and nothing ever says so. The plan's first design
106
+ * tried to DERIVE that from `updated_at` — but the normal task-emission path in
107
+ * `next()` writes no state, so `updated_at` stays pinned at the transition for
108
+ * the whole of a healthy build. A threshold long enough to avoid false positives
109
+ * would be far too long to catch the stall it exists for.
110
+ *
111
+ * So porch records the fact instead. This runs on the first `porch next` that
112
+ * reaches the NORMAL path after a boundary — which only happens if a builder came
113
+ * back and asked for work. A boundary recorded but never acknowledged therefore
114
+ * means exactly one thing: nobody returned.
115
+ *
116
+ * ## Why porch owns it
117
+ *
118
+ * The builder-side command is forbidden from writing `status.yaml`, and that
119
+ * prohibition is load-bearing — it is what makes a failed refresh incapable of
120
+ * corrupting protocol state. So the acknowledgment cannot come from the side that
121
+ * would most naturally report "I'm back"; it has to be inferred by the only
122
+ * writer, from the only evidence it has: the builder asked for its next task.
123
+ *
124
+ * Writes at most once per boundary, so this costs one extra state write per
125
+ * refresh rather than one per `porch next`.
126
+ *
127
+ * ## What this cannot distinguish, and why that is accepted
128
+ *
129
+ * Any normal-path pass acknowledges EVERY outstanding boundary. So a refresh
130
+ * that was refused before clearing — after which the builder simply runs `porch
131
+ * next` — reads as acknowledged, as does a stall a human recovered by hand.
132
+ *
133
+ * That is not a bug to fix here, because porch has exactly one piece of
134
+ * evidence: the builder asked for work. It cannot tell "cleared and came back"
135
+ * from "never cleared" from "was rescued", and inventing a distinction it cannot
136
+ * observe would be worse than admitting the limit. What the signal genuinely
137
+ * means is narrow and still useful: **no builder has asked for work since this
138
+ * boundary was recorded.**
139
+ */
140
+ export function acknowledgeRefreshes(state, now) {
141
+ let changed = false;
142
+ for (const record of state.context_refreshes ?? []) {
143
+ if (record.acknowledged_at === undefined) {
144
+ record.acknowledged_at = now;
145
+ changed = true;
146
+ }
147
+ }
148
+ return changed;
149
+ }
150
+ /**
151
+ * How long a refresh may legitimately take before silence means trouble.
152
+ *
153
+ * A refresh is not instant: the builder writes a save (minutes, for a real one),
154
+ * the re-entry is held by Tower for {@link DEFAULT_REENTRY_DELAY_SECONDS}, the
155
+ * mailbox waits for a clean prompt, and only then does the builder run `porch
156
+ * next`. Ten minutes is comfortably past all of that and still far sooner than a
157
+ * human would otherwise notice a builder sitting cleared and idle.
158
+ */
159
+ export const REFRESH_STALL_GRACE_MS = 10 * 60 * 1000;
160
+ /**
161
+ * Boundaries recorded but never acknowledged — the raw fact, no timing applied.
162
+ *
163
+ * Use this for HISTORY (what has and has not been acknowledged). For deciding
164
+ * whether to warn a human, use {@link stalledRefreshes}, which adds the grace
165
+ * period.
166
+ */
167
+ export function unacknowledgedRefreshes(state) {
168
+ return (state.context_refreshes ?? [])
169
+ .filter(r => r.acknowledged_at === undefined)
170
+ .map(r => ({ boundary: r.boundary, at: r.at }));
171
+ }
172
+ /**
173
+ * Unacknowledged boundaries old enough that silence is a problem.
174
+ *
175
+ * ## Why a grace period after all
176
+ *
177
+ * An earlier version of this warned on ANY unacknowledged boundary, with a
178
+ * comment arguing that time-based rules do not work here. That argument
179
+ * conflated two different things, and the result was a warning on every healthy
180
+ * refresh — a false positive during completely normal operation, every single
181
+ * time.
182
+ *
183
+ * What genuinely does not work is deriving the stall from `updated_at`, because
184
+ * that timestamp does not move during a healthy build. But the ACKNOWLEDGMENT
185
+ * does move, exactly when the builder returns. Given a reliable event, a grace
186
+ * period is simply the answer to "has enough time passed that silence is
187
+ * suspicious?" — which is the question, and which the plan asked for in the
188
+ * words "past a threshold".
189
+ *
190
+ * A signal that fires during normal operation is not a signal.
191
+ */
192
+ export function stalledRefreshes(state, now, graceMs = REFRESH_STALL_GRACE_MS) {
193
+ return unacknowledgedRefreshes(state)
194
+ .map(r => ({ ...r, ageMs: now - Date.parse(r.at) }))
195
+ // An unparseable timestamp yields NaN, and every NaN comparison is false —
196
+ // so it would never warn. Treat it as stalled rather than silently ignoring
197
+ // it: a record we cannot age is a record we cannot vouch for.
198
+ .filter(r => Number.isNaN(r.ageMs) || r.ageMs >= graceMs);
199
+ }
200
+ // ============================================================================
201
+ // Decision
202
+ // ============================================================================
203
+ /**
204
+ * Should a refresh fire for `boundary`, and is it declared?
205
+ *
206
+ * Deliberately does NOT consider how much context the builder has used. The spec
207
+ * fixes always-fire-at-configured-boundaries over threshold triggering:
208
+ * thresholds would need harness introspection porch does not have, and would make
209
+ * the behavior non-deterministic and untestable. An unnecessary refresh at a
210
+ * clean boundary costs a re-read of artifacts the builder can already see.
211
+ */
212
+ export function shouldRefresh(state, declared, boundary) {
213
+ return declared && !hasRefreshed(state, boundary);
214
+ }
215
+ // ============================================================================
216
+ // The task
217
+ // ============================================================================
218
+ /**
219
+ * The single task emitted at a boundary.
220
+ *
221
+ * Deliberately does NOT instruct `porch done`. A refresh is not a build: `porch
222
+ * done` would validate checks and advance the build state, which is wrong at a
223
+ * moment when nothing was built. The builder refreshes and then re-enters through
224
+ * `porch next`, which is also what the post-clear re-entry frame says — so both
225
+ * paths converge on the same next command whether or not the clear lands.
226
+ */
227
+ export function buildRefreshTask(boundary) {
228
+ // Single source: see `lib/self-refresh-invocation.ts` for why this is not
229
+ // typed out here. Two hand-written copies have already dropped the flag.
230
+ const invocation = selfRefreshInvocation(boundary);
231
+ return {
232
+ subject: 'Refresh your context',
233
+ activeForm: 'Refreshing context',
234
+ description: [
235
+ `CONTEXT REFRESH BOUNDARY: \`${boundary}\``,
236
+ '',
237
+ 'You have reached a protocol boundary where your context can be safely refreshed.',
238
+ 'Your durable state is already on disk — the spec, the plan, `status.yaml`, your',
239
+ 'thread narrative and git history — so a fresh context re-orients from artifacts',
240
+ 'rather than from memory.',
241
+ '',
242
+ 'Run the builder-refresh procedure:',
243
+ '',
244
+ '```bash',
245
+ `${invocation.begin} # issue the challenge`,
246
+ '# ...write your working state to the file it names...',
247
+ `${invocation.execute} # verify, then clear and re-orient`,
248
+ '```',
249
+ '',
250
+ 'Pass `--boundary` on BOTH commands. It binds the challenge to this boundary, so a',
251
+ 'challenge left behind by an aborted refresh cannot be used to clear you at a later',
252
+ 'one — against a save describing work that has since moved on.',
253
+ '',
254
+ 'If your harness provides the `/builder-refresh` skill, invoke that instead — it',
255
+ 'sequences the same steps.',
256
+ '',
257
+ 'IMPORTANT:',
258
+ `- This boundary is already recorded in \`status.yaml\`. It will NOT fire again,`,
259
+ ' whether the refresh succeeds or fails.',
260
+ '- Do NOT run `porch done` for this task. A refresh is not a build.',
261
+ '- If the refresh REFUSES (it fails safe rather than clearing on an unverified',
262
+ ' save), you keep your context. Report the refusal to the architect with',
263
+ ' `afx send architect "..."` and continue — run `porch next` for your normal tasks.',
264
+ '- After a successful refresh your next instruction arrives automatically. It will',
265
+ ' tell you to run `porch next`.',
266
+ ].join('\n'),
267
+ sequential: true,
268
+ };
269
+ }
270
+ //# sourceMappingURL=context-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-refresh.js","sourceRoot":"","sources":["../../../src/commands/porch/context-refresh.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,12 @@ import { type ArtifactResolver } from './artifacts.js';
27
27
  * "gate_requested_at": string | null, // ISO timestamp
28
28
  * "gate_approved_at": string | null // ISO timestamp
29
29
  * }
30
+ *
31
+ * `--json` also carries the Spec 1470 refresh fields: `context_refreshes` (the
32
+ * full history, each with `boundary`, `at`, and `acknowledged_at` once a builder
33
+ * has returned), `unacknowledged_refreshes` (the raw fact, any age), and
34
+ * `stalled_refreshes` (only those past the grace period, each with `ageMs`).
35
+ * Fields are ADDED; nothing pre-existing is removed or retyped.
30
36
  */
31
37
  export declare function status(workspaceRoot: string, projectId: string, resolver?: ArtifactResolver, options?: {
32
38
  json?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/porch/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAqCH,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAiFnF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,MAAM,CAC1B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsHf;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoDhH;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+L5K;AAsED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD/G;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,OAAO,EACrB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA4Hf;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA+Ef;AAED;;;;;;;GAOG;AACH,wBAAsB,IAAI,CACxB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Cf;AA0CD;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ElE;AAoBD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6KvD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/porch/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsCH,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAiFnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,MAAM,CAC1B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqKf;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoDhH;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+L5K;AAsED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD/G;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,OAAO,EACrB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA4Hf;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA+Ef;AAED;;;;;;;GAOG;AACH,wBAAsB,IAAI,CACxB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Cf;AA0CD;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ElE;AAoBD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6KvD"}
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import chalk from 'chalk';
10
10
  import { globSync } from 'glob';
11
+ import { stalledRefreshes, unacknowledgedRefreshes } from './context-refresh.js';
11
12
  import { readState, writeStateAndCommit, createInitialState, findStatusPath, getArtifactRoot, getProjectDir, getStatusPath, resolveProjectId, resolveArtifactBaseName, } from './state.js';
12
13
  import { loadProtocol, getPhaseConfig, getNextPhase, getPhaseChecks, getPhaseGate, isPhased, isBuildVerify, getVerifyConfig, } from './protocol.js';
13
14
  import { findPlanFile, extractPlanPhases, getCurrentPlanPhase, getPhaseContent, allPlanPhasesComplete, } from './plan.js';
@@ -99,6 +100,12 @@ function logCheckOverrides(phaseCheckNames, resolvedChecks, overrides) {
99
100
  * "gate_requested_at": string | null, // ISO timestamp
100
101
  * "gate_approved_at": string | null // ISO timestamp
101
102
  * }
103
+ *
104
+ * `--json` also carries the Spec 1470 refresh fields: `context_refreshes` (the
105
+ * full history, each with `boundary`, `at`, and `acknowledged_at` once a builder
106
+ * has returned), `unacknowledged_refreshes` (the raw fact, any age), and
107
+ * `stalled_refreshes` (only those past the grace period, each with `ageMs`).
108
+ * Fields are ADDED; nothing pre-existing is removed or retyped.
102
109
  */
103
110
  export async function status(workspaceRoot, projectId, resolver, options) {
104
111
  const statusPath = findStatusPath(workspaceRoot, projectId);
@@ -126,6 +133,11 @@ export async function status(workspaceRoot, projectId, resolver, options) {
126
133
  gate_status: gateStatus?.status ?? null,
127
134
  gate_requested_at: gateStatus?.requested_at ?? null,
128
135
  gate_approved_at: gateStatus?.approved_at ?? null,
136
+ // Spec 1470. Fields ADDED, none removed or retyped, so existing consumers
137
+ // (dashboard, VS Code tree) keep parsing unchanged.
138
+ context_refreshes: state.context_refreshes ?? [],
139
+ unacknowledged_refreshes: unacknowledgedRefreshes(state),
140
+ stalled_refreshes: stalledRefreshes(state, Date.now()),
129
141
  };
130
142
  process.stdout.write(JSON.stringify(out) + '\n');
131
143
  return;
@@ -156,6 +168,40 @@ export async function status(workspaceRoot, projectId, resolver, options) {
156
168
  const title = isCurrent ? chalk.bold(phase.title) : phase.title;
157
169
  console.log(`${prefix}${icon(phase.status)} ${phase.id}: ${title}`);
158
170
  }
171
+ }
172
+ // Context refreshes (Spec 1470). Shown for any protocol that has them, not
173
+ // only phased ones — a boundary can fire on entering `plan` too.
174
+ const refreshes = state.context_refreshes ?? [];
175
+ if (refreshes.length > 0) {
176
+ const stalled = stalledRefreshes(state, Date.now());
177
+ console.log('');
178
+ console.log(chalk.bold('CONTEXT REFRESHES:'));
179
+ console.log('');
180
+ for (const r of refreshes) {
181
+ const done = r.acknowledged_at !== undefined;
182
+ const isStalled = stalled.some(sr => sr.boundary === r.boundary);
183
+ // Three states, not two: acknowledged, in flight, and stalled. Marking an
184
+ // in-flight refresh as a fault would cry wolf on every healthy one.
185
+ const mark = done ? chalk.green('✓') : isStalled ? chalk.yellow('!') : chalk.cyan('…');
186
+ const when = done
187
+ ? ''
188
+ : isStalled
189
+ ? chalk.yellow(' ← no builder has returned since')
190
+ : chalk.cyan(' ← refresh in flight');
191
+ console.log(` ${mark} ${r.boundary} ${chalk.gray(r.at)}${when}`);
192
+ }
193
+ if (stalled.length > 0) {
194
+ // The unattended failure this whole field exists to surface: the builder
195
+ // cleared and nothing came back. Say what to do, because the person
196
+ // reading this is not necessarily the person who built the feature.
197
+ console.log('');
198
+ console.log(chalk.yellow(` ⚠ ${stalled.length} refresh(es) recorded but never acknowledged. A builder that ` +
199
+ `cleared and did not return looks idle, not broken.`));
200
+ console.log(chalk.gray(' Recover with: ') +
201
+ `afx send <builder> "Read .builder-reorient.md, then run porch next"`);
202
+ }
203
+ }
204
+ {
159
205
  const currentPlanPhase = getCurrentPlanPhase(state.plan_phases);
160
206
  if (currentPlanPhase) {
161
207
  console.log('');