@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.
- package/dashboard-dist/assets/index-D3XaKpZW.js.map +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +52 -1
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/reset/constants.d.ts +118 -0
- package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/constants.js +118 -0
- package/dist/agent-farm/commands/reset/constants.js.map +1 -1
- package/dist/agent-farm/commands/reset/context.d.ts +18 -0
- package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/context.js +41 -0
- package/dist/agent-farm/commands/reset/context.js.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.js +16 -5
- package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.js +8 -1
- package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
- package/dist/agent-farm/commands/reset/self.d.ts +335 -0
- package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/self.js +636 -0
- package/dist/agent-farm/commands/reset/self.js.map +1 -0
- package/dist/agent-farm/commands/reset.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset.js +23 -18
- package/dist/agent-farm/commands/reset.js.map +1 -1
- package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
- package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
- package/dist/agent-farm/commands/self-refresh.js +506 -0
- package/dist/agent-farm/commands/self-refresh.js.map +1 -0
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +27 -2
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +7 -0
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +13 -1
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +12 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +12 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +141 -13
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +172 -16
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +23 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +91 -31
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +58 -18
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +73 -28
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +13 -0
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +21 -0
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +220 -27
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +333 -42
- package/dist/agent-farm/servers/session-submit.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +114 -25
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +14 -0
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/types.d.ts +36 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +60 -4
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +115 -22
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +8 -2
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +19 -2
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/commands/porch/context-refresh.d.ts +181 -0
- package/dist/commands/porch/context-refresh.d.ts.map +1 -0
- package/dist/commands/porch/context-refresh.js +270 -0
- package/dist/commands/porch/context-refresh.js.map +1 -0
- package/dist/commands/porch/index.d.ts +6 -0
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +46 -0
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +0 -9
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +133 -4
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +102 -0
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/types.d.ts +61 -0
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/lib/self-refresh-invocation.d.ts +60 -0
- package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
- package/dist/lib/self-refresh-invocation.js +64 -0
- package/dist/lib/self-refresh-invocation.js.map +1 -0
- package/dist/lib/test-env.d.ts +50 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +76 -0
- package/dist/lib/test-env.js.map +1 -1
- package/package.json +4 -4
- package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/protocol-schema.json +17 -0
- package/skeleton/protocols/air/protocol.json +1 -1
- package/skeleton/protocols/aspir/protocol.json +5 -1
- package/skeleton/protocols/bugfix/protocol.json +1 -1
- package/skeleton/protocols/experiment/protocol.json +1 -1
- package/skeleton/protocols/maintain/protocol.json +1 -1
- package/skeleton/protocols/pir/protocol.json +1 -1
- package/skeleton/protocols/protocol-schema.json +17 -0
- package/skeleton/protocols/research/protocol.json +1 -1
- package/skeleton/protocols/spike/protocol.json +1 -1
- 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;
|
|
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('');
|