Skip to content

Commit 00a49ad

Browse files
Stall detection now fires while agent is busy (#252)
* feat: apply display-length clip for goal progress summary at render time Goal progress renderer now clips run summaries to display length (120 chars), moved from orchestrator so stored events retain full text while terminal output stays single-line. Blocker and reason prose are deliberately never clipped. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> * feat: stall detection now fires while agent is busy Previously a stall was only detectable on a continuation turn that made zero tool calls. This missed the dominant real-world failure mode: the three worst stalls in the corpus (31, 24, and 54 turns) made tool calls every turn while the goal had become unsatisfiable, so the detector never fired. A second trigger, independent of tool activity, now runs alongside the original. It uses a cheap deterministic pre-filter over recent evaluator reasons every turn (no model call) and only consults the stall judge when the pre-filter trips, preserving the invariant that a mechanical condition alone never trips a stall. The stall judge was reframed from binary yes/no into a verdict taxonomy: resolvable, time-locked, structure-locked, or history-locked. The last three all mean stop now and name the dead end, which users need to rewrite the goal. The judge's prompts were split by trigger, since the original prompt hard-codes the assertion that the assistant took no tool actions. The CLI's stalled-state wording now derives from that verdict instead of from a distinct-blocker count. The old counter compared strings with only whitespace and case normalization, so an evaluator that rephrased a blocker each turn produced a distinct signature per turn, yielding a false "flailing" verdict. The evaluator reason list is now capped before being passed to the summary model; it previously grew unbounded and a 54-turn run shipped all 54 reasons. Replayed against the three recorded reason chains: new pre-filter first consults judge at turn 14 (stalled at 31), turn 12 (stalled at 24), and turn 18 (stalled at 54). On achieved organic runs from the same sessions: zero false trips. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> * feat: ship goalify skill inside CLI as version-locked packaged data goalify composes a /goal stop-condition from the current conversation and lints it against known termination-failure patterns before showing it to the user. It has a hard prerequisite — it is only meaningful when the CLI provides the /goal command — so co-locating it with the CLI satisfies that prerequisite by construction rather than by configuration. The skill lives at amplifier_app_cli/data/skills/goalify/ and is registered by appending a package-relative path in _ensure_default_skills_dirs(). Resolution is by installed package location, never by git URI: a git reference would have co-located the files in the repo while still versioning them independently of the code they depend on, which defeats the purpose. The wheel build declares packages = ["amplifier_app_cli"], so repo-root files are excluded from the distribution entirely. The skill directory is appended in Python rather than declared through a bundle's tool-skills config, because bundle-level skills config replaces rather than appends — the same hazard _ensure_default_skills_dirs was originally written to work around. Verified that workspace and user skills still load alongside the packaged one. The skill body contains only instructions to the invoking agent. Authoring rationale and the lint-rule evidence base were moved to sibling PROVENANCE.md, which load_skill does not read. The lint output table now reports "no known pattern detected" rather than "PASS", because a column of PASS reads as a validation claim the linter cannot support — it detects known patterns from a finite corpus. A repo-root AGENTS.md records the decision rule for what may live in amplifier_app_cli/data/ versus an external bundle: the asset must depend on something the CLI uniquely provides, no non-CLI host would want it, and unconditional triggers belong in the bundle layer while gated ones stay in Python. Assets resolve by package path only, and this location is auto-loaded for every user in every session, so its token budget discipline is stricter than anywhere else. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --------- Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
1 parent 9c5a58f commit 00a49ad

9 files changed

Lines changed: 653 additions & 24 deletions

File tree

AGENTS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# AGENTS.md — amplifier-app-cli
2+
3+
## Boundary rule: `amplifier_app_cli/data/` vs. an external bundle
4+
5+
`amplifier_app_cli/data/` ships inside the CLI's own wheel — anything placed there loads
6+
for every user, every session, version-locked to the installed CLI, with no bundle
7+
composition step in between. That reach is exactly why it must stay small. Before adding
8+
anything here, run it through these three tests, in order:
9+
10+
1. **Does it depend on something the CLI uniquely provides, that cannot move?** A slash
11+
command, a settings key, a terminal affordance — something with no home outside this
12+
process. If no → it belongs in an external bundle, not here.
13+
2. **Would a non-CLI host ever want it?** If yes → external bundle. The CLI may still
14+
*include* it (compose the bundle), but must not *own* it — ownership belongs wherever
15+
the capability is portable to.
16+
3. **Is the trigger unconditional?** If the asset is gated on settings, an env var, a flag,
17+
or runtime state, the asset itself may still live here, but the compose/injection
18+
*decision* stays in Python (see `runtime/config.py::_ensure_default_skills_dirs` for the
19+
pattern) — never encode conditional loading in a bundle YAML that lives alongside it.
20+
21+
If the answer to 1 is "no" or the answer to 2 is "yes," it's an external bundle question,
22+
not a `data/` question.
23+
24+
**Resolution rule:** assets under `amplifier_app_cli/data/` are always resolved by
25+
**package-relative path** (e.g. `Path(__file__).parent.parent / "data" / "..."`), **never**
26+
by git URI. A git URI decouples the asset's version from the installed wheel's version —
27+
defeating the reason for co-locating it here in the first place. If it needs independent
28+
versioning, it isn't a `data/` asset.
29+
30+
**Token budget:** this location is auto-loaded for every user, every session — its budget
31+
discipline is stricter than anywhere else in the ecosystem. No always-on context files
32+
here without an explicit, named exception recorded in this section. Prefer mechanisms
33+
that load on demand (skills, agent-scoped context) over anything injected unconditionally.
34+
35+
This section exists to keep `data/` from becoming a junk drawer — re-run the three tests
36+
before adding, not after.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# goalify — rule provenance
2+
3+
Not loaded by `load_skill`. This file exists so the lint rules in `SKILL.md`
4+
can be audited and re-derived. Read it when changing a rule, not when using
5+
the skill.
6+
7+
## Where the rules come from
8+
9+
Every BLOCKER traces to an observed `/goal` run that failed to terminate.
10+
That property is the ruleset's credibility. **Do not add a rule without a
11+
named run or a corpus measurement behind it.**
12+
13+
| Rule | Origin |
14+
|---|---|
15+
| L1 — ordering/provenance constraint | A run whose condition required `PROVEN with evidence you produced yourself`. The evaluator crystallised this into a constraint on the transcript's own past and stated it could not be retroactively repaired. Stalled after 54 turns; unreachable from roughly turn 45. |
16+
| L2 — universal quantifier with exempt members | A run requiring evidence for `all 9 sites` in a uniform structure, where one site could not structurally produce that evidence. Stalled after 24 turns. |
17+
| L3 — elapsed wall-clock requirement | A run whose only available proof standard was real-world use over time. Stalled after 31 turns. |
18+
| L4 — human-in-the-loop dependency | A condition containing `Stop and ask me if you need a decision from me`, which contradicts unattended continuation. 12 turns consumed in 35 seconds. |
19+
| L5 — open enumeration | `all editing features of Publisher` — never terminated. Its sibling run in the same session, same codebase, same day (`until I have a usable app`) achieved on turn 1. |
20+
| L0 — cross-clause consistency | The L2 run above carried a textbook four-verdict exit vocabulary and stalled anyway, because one other sentence silently overrode it. |
21+
| L6 — missing disjunctive exit | Advisory, not blocking. See below. |
22+
23+
## Why L6 is a WARNING, not a BLOCKER
24+
25+
A lint regression over 30 scored real runs with known outcomes measured L6
26+
firing on 37% of all real conditions at a 9% hit rate — the largest single
27+
source of false positives of any rule, consistent across the tuning split,
28+
the held-back split, and the full set. L6 judges exit-clause presence in
29+
isolation and cannot see turn position or residual scope, so it routinely
30+
flagged short finisher-style conditions written late in a long session.
31+
32+
On that corpus, presence of a disjunctive exit was only weakly correlated
33+
with actual termination — several conditions with strong exit language
34+
stalled anyway, for reasons L1/L2/L0 cover independently.
35+
36+
Demoting L6 raised measured precision from 20% to 43%, recall unchanged
37+
at 100%. L0 and L1–L5 each fired only 1–2 times in that evaluation — too few
38+
observations to justify changing their classification, so none was changed.
39+
40+
## How to read the precision number
41+
42+
The 20%/43% figures were measured on a corpus dominated by **human-authored**
43+
conditions, which terminate about 96% of the time. On a population that
44+
rarely fails, any linter's precision is bounded by arithmetic — a rule that
45+
flags a condition which succeeded anyway counts as a false positive.
46+
47+
This skill lints **agent-authored** conditions, which in the same corpus
48+
terminated about 60% of the time. Same rules, roughly ten times the base
49+
rate of true positives. **Treat 43% as a floor measured on the easiest
50+
available population, not as the operating precision.** Re-scoring against
51+
the agent-authored subset alone is the outstanding measurement.
52+
53+
The asymmetry also justifies the operating point: a false positive costs one
54+
in-session rewrite pass; a false negative costs a 24-to-54-turn unrepairable
55+
stall. High recall at moderate precision is the correct trade here.
56+
57+
## Standing caveats
58+
59+
- **The corpus ages.** These rules encode `/goal` evaluator semantics as
60+
observed at the time of measurement. If the goal loop's evaluator changes,
61+
nothing will automatically flag that the rules have rotted.
62+
- **Effective sample size is smaller than it looks.** The scored runs came
63+
from roughly 8 distinct sessions; runs within a session share an author, a
64+
project, and phrasing habits.
65+
- **The skill drafts; the human edits.** Automating goal authoring is itself
66+
the thing that raises failure rates (60% vs 96%). The lint is the bet that
67+
it closes that gap, and that bet has not been measured end to end. The
68+
human review step is load-bearing, which is why the skill offers and never
69+
auto-runs.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
name: goalify
3+
description: >
4+
Compose a /goal stop-condition from the current conversation and lint it
5+
against known termination-failure patterns before showing it to the user.
6+
Use when the user wants to turn the current task into a /goal loop, asks to
7+
"goalify this", wants a stop condition for autonomous work, says "write a
8+
goal condition", "make this a /goal", "turn this into a goal", or asks for
9+
help wording a condition for /goal.
10+
user-invocable: true
11+
version: 1.1.0
12+
license: MIT
13+
---
14+
15+
Run this procedure yourself, in the current conversation. Do not delegate it
16+
to a sub-agent or forked session — Phase 1 reads the live transcript.
17+
18+
$ARGUMENTS
19+
20+
If the user supplied focus text above, use it to scope Phase 1. If empty,
21+
extract from conversation alone — do not ask the user to restate what they
22+
already said.
23+
24+
---
25+
26+
## Phase 1 — Extract
27+
28+
From the conversation so far, determine:
29+
30+
- **The target end state.** What does "finished" concretely look like? It
31+
must be a state that can be checked, not an activity that can be performed
32+
indefinitely. ("a usable app" is checkable; "finish building the project"
33+
is not — there is no test for "finished building".)
34+
- **What is already done.** Re-read the transcript for completed sub-tasks,
35+
passing tests, merged changes, or resolved questions. These become
36+
candidates for the KNOWN section (Phase 2) or for narrowing scope
37+
(SCOPE-OUTS).
38+
- **What is explicitly NOT required.** Anything the user has ruled out,
39+
deferred, or said isn't needed. This is the raw material for SCOPE-OUTS.
40+
41+
If the end state genuinely cannot be determined from context (not merely
42+
effortful to determine), ask one direct question. Otherwise proceed —
43+
guessing and then showing your extraction for correction is faster than
44+
front-loading a question the transcript already answers.
45+
46+
## Phase 2 — Compose
47+
48+
Emit a candidate condition using this structure. Every element is required
49+
unless marked optional.
50+
51+
1. **One-sentence outcome** naming a checkable end state (not an activity).
52+
2. **Disjunctive exit**: the condition must be satisfiable by *either*
53+
reaching the end state *or* conclusively demonstrating it cannot be
54+
reached (naming the blocker). Never phrase a condition with only one exit.
55+
3. **Per-item negative terminal**, if the condition lists multiple items
56+
(tasks, sites, phases, experiments). Each item must be able to resolve to
57+
its own PASS / FAIL / BLOCKED-with-named-reason — a blocker on one item
58+
converts *that item* to a residual; it must not block the whole goal.
59+
4. **SCOPE-OUTS section** — an explicit list of what is *not* required. Write
60+
this by directly converting anything from Phase 1's "not required" list
61+
into a plain negative statement (e.g. "No production soak time required."
62+
/ "Uniformity across all N items is NOT the goal.").
63+
5. **KNOWN section (optional)** — facts already established, so the actor
64+
doesn't re-derive them. Label it explicitly as a speed aid: it prevents
65+
wasted turns, it does not by itself prevent stalls, so it never replaces
66+
items 1–4.
67+
68+
## Phase 3 — Lint
69+
70+
Check the full composed document against every rule below. Work through all
71+
BLOCKERS first; a document with any BLOCKER triggered is not ready to show.
72+
Then check WARNINGS, which are advisory and do not block presentation.
73+
74+
**Read the whole document for each check.** Several of these rules are only
75+
detectable by considering the document as one system — a single clause
76+
elsewhere can silently defeat a correct-looking rule everywhere else. Do not
77+
scan for keywords in isolation and stop at the first clean-looking match.
78+
79+
### BLOCKERS — fix all before presenting
80+
81+
- **L1 — Ordering/provenance constraint on the transcript's own history.**
82+
Any phrasing that requires evidence to precede, or be produced independent
83+
of, events that already exist in the transcript (e.g. "verify it yourself,
84+
then state what you verified", "proof must precede the claim", "evidence
85+
you produced yourself" applied to something already reported by a
86+
sub-agent or prior turn). This class of requirement is **unrepairable**
87+
no later turn can change what already happened earlier in the transcript,
88+
so it cannot be fixed by adding more work. If the condition constrains
89+
ordering, it must constrain only *future* actions, never re-litigate what
90+
is already in the history.
91+
92+
- **L2 — Universal quantifier over a set with possibly-exempt members.**
93+
"all N", "every X", "each of the Y", "uniform/uniformity", "complete
94+
parity", applied to a set, is a blocker **unless** each item individually
95+
carries a negative terminal (see Compose #3) or the condition names which
96+
members are exempt and why. Without one of those, a single member that
97+
cannot structurally produce the required evidence makes the whole
98+
condition permanently unsatisfiable.
99+
100+
- **L3 — Elapsed wall-clock requirement.** Anything that requires real time
101+
to pass beyond the current session: "production soak", "after N days of
102+
use", "monitor over time", "verify in real-world use". A single session
103+
cannot advance wall-clock time; this can never be satisfied in-session.
104+
105+
- **L4 — Human-in-the-loop or external-actor dependency mid-loop.**
106+
"stop and ask me if you need a decision", "once a reviewer merges this",
107+
"wait for approval before continuing". This directly conflicts with
108+
unattended continuation — the loop will halt waiting on an event that a
109+
condition-checking loop cannot itself produce.
110+
111+
- **L5 — Open enumeration.** Scope phrased as an unbounded or unenumerated
112+
set: "all editing features of X", "complete parity with Y", "everything
113+
needed to fully support Z". An evaluator can always name one more item
114+
under this phrasing, so it never terminates. Convert to a closed, named
115+
list, or to a single representative artifact.
116+
117+
- **L0 — Cross-clause consistency (meta-rule).** *An escape hatch is only as
118+
strong as the strictest other clause in the same document.* After
119+
confirming L1–L5 pass individually and a disjunctive exit exists, re-read
120+
the document once more asking only: **is there any other sentence, anywhere
121+
in the document, that is stricter than the stated exit and would override
122+
it?** A document can have a textbook-perfect exit clause and still be
123+
unsatisfiable because one unrelated sentence elsewhere re-imposes an L1–L5
124+
style constraint the exit clause doesn't cover. Confirming an exit clause
125+
exists is not sufficient — confirm nothing else in the document is
126+
stricter than it.
127+
128+
### WARNINGS — advisory, do not block presentation
129+
130+
- **L6 — Missing disjunctive exit.** The document should state achievement
131+
*or* a way to conclusively end in "not achievable, here is why" (see
132+
Compose #2). Flag and fix its absence where practical, but do not block
133+
presentation on it alone.
134+
- **W1** — Multiple items are listed but not all of them carry their own
135+
negative terminal (some do, some don't).
136+
- **W2** — No clause asking the actor to show evidence inline in the
137+
transcript as it's produced, rather than only asserting a result.
138+
- **W3** — Scope reads like more than one session's worth of work (multi-week
139+
rollout language, coordination across many independent repos/teams,
140+
phased production deployment).
141+
- **W4** — The condition contains a cautionary anecdote or narrative about a
142+
failure mode (e.g. "don't repeat what went wrong last time", "make sure
143+
this doesn't stall like before") rather than a plain instruction. Any such
144+
narrative addressed to the actor is read by the evaluator too, and can
145+
silently become a criterion the evaluator judges against instead of
146+
guidance the actor merely follows. State requirements as plain criteria,
147+
never as stories.
148+
149+
**This applies to the condition you are composing right now.** Write every
150+
clause as a direct instruction to the actor, never as a story about a past
151+
run. If you catch yourself writing "so that we don't repeat X", rewrite it
152+
as the direct requirement it implies, with no reference to the incident.
153+
154+
### If a BLOCKER cannot be cleared
155+
156+
Rewrite and re-check. Allow up to three rewrite passes. If a BLOCKER still
157+
fires after three passes, stop and surface the specific tension to the user
158+
by name (e.g. "the user's own request requires enumerating an open-ended set
159+
— L5 fires no matter how I phrase it; how would you like to bound this?").
160+
Do not present a condition that still fails a BLOCKER.
161+
162+
---
163+
164+
## Output format
165+
166+
Always output the condition inside a fenced code block — terminal reflow
167+
will otherwise destroy its multi-line structure. Follow it with the lint
168+
report as a table, then offer (do not auto-run) `/goal`.
169+
170+
```
171+
<the condition text>
172+
```
173+
174+
| Rule | Result | Note |
175+
|------|--------|------|
176+
| L0 | no known pattern detected | ... |
177+
| L1 | no known pattern detected | ... |
178+
| L2 | no known pattern detected | ... |
179+
| L3 | no known pattern detected | ... |
180+
| L4 | no known pattern detected | ... |
181+
| L5 | no known pattern detected | ... |
182+
| L6, W1–W4 | (list only the ones that fired) | ... |
183+
184+
A clean table means no *known* failure pattern was detected — not that the
185+
condition is validated. Say so if the user reads it as a guarantee.
186+
187+
Then: "Pass this to `/goal` to start the loop — want me to run it now, or
188+
would you like to adjust anything first?"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Known-bad example — L1 (ordering/provenance constraint)
2+
3+
Used to sanity-check the lint in `SKILL.md` Phase 3. This condition is
4+
modeled on a real run that stalled for many turns because the ordering
5+
constraint it imposes on the transcript's own history became impossible to
6+
satisfy once evidence had already been reported by a sub-agent.
7+
8+
## Condition text
9+
10+
```
11+
Done when the migration is PROVEN complete, with evidence you produced
12+
yourself. A sub-agent's report is not proof. Verify it yourself, then state
13+
what you verified — or show a named blocker and stop.
14+
```
15+
16+
## Expected lint result
17+
18+
- **L1: FAIL** — "with evidence you produced yourself" combined with "verify
19+
it yourself, then state what you verified" imposes an ordering constraint
20+
on the transcript's own history. If a sub-agent already reported a result
21+
earlier in the transcript, no later turn can retroactively make that
22+
report "verified by you first" — the requirement is unrepairable once the
23+
transcript already contains the sub-agent's report.
24+
- L6 present (disjunctive exit exists: "or show a named blocker and stop"),
25+
but L1 alone should be sufficient to fail this condition.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Known-bad example — L2 / L0 (universal quantifier + cross-clause override)
2+
3+
Used to sanity-check the lint in `SKILL.md` Phase 3. Modeled on a real run
4+
that stalled because a uniform requirement applied to every member of a set
5+
made the condition permanently unsatisfiable once one member could not
6+
structurally produce the required evidence.
7+
8+
## Condition text
9+
10+
```
11+
Done when all 9 sites are migrated in a uniform structure, verified working,
12+
or proven impossible with a named blocker.
13+
```
14+
15+
## Expected lint result
16+
17+
- **L2: FAIL** — "all 9 sites" + "uniform structure" is a universal
18+
quantifier over a set, with no per-item negative terminal and no named
19+
exemption for a site that cannot physically satisfy "uniform structure."
20+
- **L0: FAIL** — the document has a disjunctive-looking exit ("or proven
21+
impossible with a named blocker"), but that exit applies to the *goal as a
22+
whole*, not per-site. The stricter clause ("uniform structure" across all
23+
9) is not covered by the escape hatch, because the escape hatch only
24+
fires once — it can't let 8 sites succeed uniformly while 1 is blocked and
25+
still call the set "uniform." Confirming the exit clause exists is not
26+
enough; it does not cover the stricter clause.
27+
- L6 nominally present (a disjunctive-shaped phrase exists) but does not
28+
actually rescue the condition — this is exactly the L0 case: an escape
29+
hatch that reads as satisfied but is overridden by a stricter clause
30+
elsewhere in the same document.

0 commit comments

Comments
 (0)