@event4u/agent-config 14.1.0 → 14.4.0
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +291 -181
- package/agents/templates/.ai-council.yml.example +35 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
- package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
- package/dist/agent-src/scripts/resume_probe.ts +154 -9
- package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
- package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
- package/dist/agent-src/templates/agent-settings.md +70 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/dist/cli/python/workspace_drive.js +8 -3
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +16 -12
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
- package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
- package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
- package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +26 -11
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +17 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +3773 -1731
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1070 -408
- package/dist/scripts/_lib/host_launch.js +66 -0
- package/dist/scripts/_lib/host_launch.js.map +1 -0
- package/dist/server/io/yamlIO.js +175 -3
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/wizard.js +20 -1
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/settingsConsent.js +25 -0
- package/dist/shared/settingsConsent.js.map +1 -1
- package/docs/contracts/settings-classes.md +11 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/resume_probe.ts +154 -9
- package/src/agent-src/scripts/roadmap_gates.ts +210 -6
- package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/src/config/estate-count-budget.json +120 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +32 -4
- package/src/config/gate-violation-baselines.json +63 -63
- package/src/config/gitignore-block.txt +5 -0
- package/src/config/hook-latency-budget.json +40 -1
- package/src/config/hook-token-budget.json +22 -0
- package/src/config/rule-stub-ceilings.json +11 -3
- package/src/scripts/_dispatch.bash +42 -1
- package/src/scripts/_lib/council_fallback_posture.ts +81 -0
- package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
- package/src/scripts/_lib/council_settings_block.ts +152 -0
- package/src/scripts/_lib/headless_invocation.ts +285 -0
- package/src/scripts/_lib/host_launch.ts +72 -0
- package/src/scripts/_lib/map_to_object.ts +16 -0
- package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
- package/src/scripts/_lib/run_checkpoint.ts +409 -0
- package/src/scripts/_lib/session_register.ts +32 -1
- package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
- package/src/scripts/_lib/stdin.ts +35 -1
- package/src/scripts/_lib/turn_end_refusals.ts +12 -4
- package/src/scripts/_lib/unattended_guard.ts +371 -0
- package/src/scripts/ai_council/config.ts +27 -1
- package/src/scripts/ai_council/events_log.ts +12 -1
- package/src/scripts/ai_council/fallback_config.ts +114 -0
- package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
- package/src/scripts/ai_council/orchestrator.ts +251 -292
- package/src/scripts/ai_council/py_parity.ts +139 -0
- package/src/scripts/ai_council/response_render.ts +99 -0
- package/src/scripts/ai_council/spend_gate.ts +123 -0
- package/src/scripts/ai_council/transport_resolver.ts +81 -13
- package/src/scripts/bench_hook_injection.ts +100 -6
- package/src/scripts/bench_hook_latency.ts +442 -16
- package/src/scripts/check_estate_count.ts +924 -0
- package/src/scripts/compile_hook_manifest.ts +67 -0
- package/src/scripts/council_cli.ts +100 -125
- package/src/scripts/decision_memo.ts +325 -0
- package/src/scripts/dispatch_r2_reviewer.ts +169 -15
- package/src/scripts/hook_manifest.json +1 -0
- package/src/scripts/hook_manifest.yaml +172 -25
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +183 -10
- package/src/scripts/hooks/dispatch_issues.ts +26 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
- package/src/scripts/hooks/hook_stdin.ts +111 -5
- package/src/scripts/hooks/injection_budget.ts +595 -0
- package/src/scripts/hooks/payload_stub.ts +446 -0
- package/src/scripts/hooks/run_continuation_hook.ts +627 -0
- package/src/scripts/hooks/session_eol_hook.ts +52 -0
- package/src/scripts/hooks/skill_route_hook.ts +172 -11
- package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
- package/src/scripts/hooks_doctor.ts +16 -2
- package/src/scripts/install-hooks.sh +26 -0
- package/src/scripts/interruption_report.ts +330 -13
- package/src/scripts/lint_hook_manifest.ts +167 -0
- package/src/scripts/roadmap_progress_hook.ts +254 -46
- package/src/scripts/run_supervise.ts +623 -0
- package/src/scripts/session_register_hook.ts +108 -8
- package/src/scripts/sessions_cli.ts +18 -6
- package/src/scripts/telemetry_disclosure_hook.ts +244 -0
- package/src/server/io/yamlIO.ts +167 -3
- package/src/shared/settingsConsent.ts +49 -2
- package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
|
@@ -33,12 +33,19 @@
|
|
|
33
33
|
* @see docs/contracts/settings-classes.md
|
|
34
34
|
*/
|
|
35
35
|
import { isConservativeDefault } from './settingsClasses.js';
|
|
36
|
+
/** Keys `org-pack` provenance may grant. ADR-233 § D2. */
|
|
37
|
+
const ORG_PACK_KEY_PREFIX = 'telemetry.remote.';
|
|
36
38
|
/**
|
|
37
39
|
* The sources that can carry a human decision.
|
|
38
40
|
*
|
|
39
41
|
* `jit-answer` is a question the user answered. `gui` is a human at the settings
|
|
40
42
|
* editor. `manual` is a hand-edit or an explicit CLI write on the user's
|
|
41
43
|
* instruction. `auto-detected` is excluded on purpose — see the module header.
|
|
44
|
+
*
|
|
45
|
+
* `org-pack` is NOT in this list, and its absence is not an oversight: it
|
|
46
|
+
* grants, but only for one namespace, so it is handled by its own branch in
|
|
47
|
+
* `consentVerdict` rather than by membership here. Putting it in this list
|
|
48
|
+
* would make it grant everywhere, which is the erosion ADR-233 § D2 refuses.
|
|
42
49
|
*/
|
|
43
50
|
const HUMAN_SOURCES = ['jit-answer', 'gui', 'manual'];
|
|
44
51
|
/**
|
|
@@ -55,6 +62,21 @@ export function consentVerdict(query) {
|
|
|
55
62
|
if (isConservativeDefault(query.value)) {
|
|
56
63
|
return 'withheld-default';
|
|
57
64
|
}
|
|
65
|
+
// The org-pack branch runs BEFORE the hand-edit branch, and the order is
|
|
66
|
+
// the fix rather than a style choice. `handEdited` is true for a
|
|
67
|
+
// project-local `.agent-settings.yml` — which is precisely the file an org
|
|
68
|
+
// pack ships — so with hand-edit first, a recorded `org-pack` provenance
|
|
69
|
+
// on an out-of-scope key would be granted by the wrong branch and ADR-233
|
|
70
|
+
// § D2's namespace scope would hold for no real deployment. A recorded
|
|
71
|
+
// source is the more specific fact and decides first.
|
|
72
|
+
if (query.source === 'org-pack') {
|
|
73
|
+
// A human decided, but not the human this value binds — so the grant
|
|
74
|
+
// is real and narrow. An absent key cannot be shown to be in scope,
|
|
75
|
+
// and unproven is withheld here as everywhere else in this function.
|
|
76
|
+
return query.key !== undefined && query.key.startsWith(ORG_PACK_KEY_PREFIX)
|
|
77
|
+
? 'granted'
|
|
78
|
+
: 'withheld-org-pack-out-of-scope';
|
|
79
|
+
}
|
|
58
80
|
if (query.handEdited === true) {
|
|
59
81
|
return 'granted';
|
|
60
82
|
}
|
|
@@ -86,6 +108,9 @@ export function withheldReason(verdict, key) {
|
|
|
86
108
|
return `${key} permits this, but nothing records who decided it. Ask, or set it via the GUI.`;
|
|
87
109
|
case 'withheld-machine-inferred':
|
|
88
110
|
return `${key} was auto-detected, not decided. A machine inference is not a consent.`;
|
|
111
|
+
case 'withheld-org-pack-out-of-scope':
|
|
112
|
+
return `${key} carries an org-pack consent, which grants only under `
|
|
113
|
+
+ `${ORG_PACK_KEY_PREFIX}* (ADR-233). Ask this user directly.`;
|
|
89
114
|
case 'not-a-consent-key':
|
|
90
115
|
return `${key} is not a class-B consent key — this gate does not apply to it.`;
|
|
91
116
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settingsConsent.js","sourceRoot":"","sources":["../../src/shared/settingsConsent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,qBAAqB,EAAsB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"settingsConsent.js","sourceRoot":"","sources":["../../src/shared/settingsConsent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,qBAAqB,EAAsB,MAAM,sBAAsB,CAAC;AAejF,0DAA0D;AAC1D,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,MAAM,aAAa,GAA6B,CAAC,YAAY,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAiDhF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmB;IAC9C,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QACpB,OAAO,mBAAmB,CAAC;IAC/B,CAAC;IACD,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,kBAAkB,CAAC;IAC9B,CAAC;IACD,yEAAyE;IACzE,iEAAiE;IACjE,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,sDAAsD;IACtD,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAC9B,qEAAqE;QACrE,oEAAoE;QACpE,qEAAqE;QACrE,OAAO,KAAK,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,mBAAmB,CAAC;YACvE,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,gCAAgC,CAAC;IAC3C,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,qBAAqB,CAAC;IACjC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACxC,OAAO,2BAA2B,CAAC;IACvC,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,KAAmB;IAC9C,OAAO,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAuB,EAAE,GAAW;IAC/D,QAAQ,OAAO,EAAE,CAAC;QACd,KAAK,SAAS;YACV,OAAO,IAAI,CAAC;QAChB,KAAK,kBAAkB;YACnB,OAAO,GAAG,GAAG,sEAAsE,CAAC;QACxF,KAAK,qBAAqB;YACtB,OAAO,GAAG,GAAG,gFAAgF,CAAC;QAClG,KAAK,2BAA2B;YAC5B,OAAO,GAAG,GAAG,wEAAwE,CAAC;QAC1F,KAAK,gCAAgC;YACjC,OAAO,GAAG,GAAG,wDAAwD;kBAC/D,GAAG,mBAAmB,sCAAsC,CAAC;QACvE,KAAK,mBAAmB;YACpB,OAAO,GAAG,GAAG,iEAAiE,CAAC;IACvF,CAAC;AACL,CAAC"}
|
|
@@ -34,6 +34,17 @@ who consented and when. `expires_at` / `revoked_at` / contract-version fields
|
|
|
34
34
|
stay deliberately unbuilt until a reader needs them — a field nothing reads is
|
|
35
35
|
surface without a consumer.
|
|
36
36
|
|
|
37
|
+
**A fifth source exists on the READ side only: `org-pack`** (ADR-233). It
|
|
38
|
+
records that a human org administrator decided, which is a consent — but one
|
|
39
|
+
given by someone other than the person it binds, so it grants **only** for
|
|
40
|
+
keys under `telemetry.remote.*` and withholds everywhere else. It is
|
|
41
|
+
deliberately absent from `settings:set`'s own `ProvenanceSource`, so
|
|
42
|
+
`--source org-pack` is rejected like a typo and no agent-reachable path can
|
|
43
|
+
write it; the org-pack install route writes the sidecar entry directly. That
|
|
44
|
+
asymmetry between the reader's vocabulary and the writer's is the safety
|
|
45
|
+
property rather than an inconsistency — `auto-detected` remains never-consent
|
|
46
|
+
verbatim, and this class must not become the precedent that erodes it.
|
|
47
|
+
|
|
37
48
|
## What the fence covers — and what it does not
|
|
38
49
|
|
|
39
50
|
**It governs writes, not reads.** Every class is readable by everything. A C
|
|
@@ -233,7 +233,8 @@ When the closure check fires (`count_open == 0` and `count_deferred > 0`), the a
|
|
|
233
233
|
3. **Keep in this archive** — confirm the deferred items stay searchable in the archived file; no follow-up roadmap. Choosing this records an explicit decision-to-drop in the same reply.
|
|
234
234
|
4. **Restore selected items to `[ ]`** — finish them in this roadmap before archive.
|
|
235
235
|
5. **Convert selected items to `[-]` cancelled** — drop them with rationale recorded inline.
|
|
236
|
-
3.
|
|
236
|
+
3. Route by the **preservation test** — full table, recording contract and residual limit in [`roadmap-progress-sync § Who resolves it`](../../../src/rules/roadmap-progress-sync.md). In one line: options 1, 2, 4 keep the item alive → council; 3 and 5 drop or weaken it → user, always; in doubt, user.
|
|
237
|
+
4. Only after the deferrals are resolved does the `git mv` to `archive/` run. The dashboard regen happens after the resolution, not before.
|
|
237
238
|
|
|
238
239
|
The migration mechanics (file naming, frontmatter pattern, body shape, parent-back-link) live in `roadmap-management § Spawn follow-up from deferred items`. The rule owns the obligation; the skill owns the procedure.
|
|
239
240
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.4.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -16,14 +16,19 @@
|
|
|
16
16
|
*
|
|
17
17
|
* ## What it can and cannot decide, stated plainly
|
|
18
18
|
*
|
|
19
|
-
* A resume condition is prose.
|
|
20
|
-
*
|
|
19
|
+
* A resume condition is prose. TWO shapes are machine-decidable — a roadmap
|
|
20
|
+
* reference, and a single repo-relative file path under an existence predicate
|
|
21
|
+
* — and the rest are not, so the verdict set has three values rather than two:
|
|
21
22
|
*
|
|
22
23
|
* - `fired` — every roadmap the condition names has archived, or the
|
|
23
|
-
* named step in a still-active roadmap is ticked
|
|
24
|
+
* named step in a still-active roadmap is ticked; or the
|
|
25
|
+
* single path it names under `exists` is there.
|
|
24
26
|
* - `unmet` — at least one named roadmap is still active with the
|
|
25
|
-
* named step (or the whole file) still open
|
|
26
|
-
*
|
|
27
|
+
* named step (or the whole file) still open; or the single
|
|
28
|
+
* path it names under `exists` is absent.
|
|
29
|
+
* - `undecidable` — the condition names neither, names both (a conjunction
|
|
30
|
+
* this probe can only half-weigh), names two of either, or
|
|
31
|
+
* states a predicate other than existence over its path.
|
|
27
32
|
*
|
|
28
33
|
* `undecidable` is reported as its own count and never folded into "nothing
|
|
29
34
|
* fired". A probe that reads 44 files, understands 12 of them and announces
|
|
@@ -84,6 +89,31 @@ const COMPOUND_RE = /\bboth\b|\band\b|\([1-9]\)|\([a-e]\)|`[ \t]*\+|\+[ \t]*`/i;
|
|
|
84
89
|
const FIELD_LABEL_RE = /\*\*[^*]+:\*\*/;
|
|
85
90
|
/** A roadmap slug as written in prose, with or without the `.md` suffix. */
|
|
86
91
|
const ROADMAP_REF_RE = /\broad-to-[a-z0-9][a-z0-9-]*\b/g;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A repo-relative path named inside backticks — the second decidable form.
|
|
95
|
+
*
|
|
96
|
+
* `PARK-PROBEABLE` (`road-to-estate-drawdown` Phase 2) means the resume
|
|
97
|
+
* condition can be tested by a script, and until this existed the probe could
|
|
98
|
+
* decide exactly one phrasing: a roadmap slug. Measured 2026-08-19 on the live
|
|
99
|
+
* estate, 42 of 44 park notes were `undecidable`, and the batch-1 sweep could
|
|
100
|
+
* only add six more — a verdict whose name promised a probe that did not read
|
|
101
|
+
* the conditions it was producing.
|
|
102
|
+
*
|
|
103
|
+
* Deliberately narrow in three ways, because a false FIRED un-parks live work:
|
|
104
|
+
* the path must be **inside backticks** (prose that merely contains a slash is
|
|
105
|
+
* not a condition), it must start at a **known top-level repo directory** and
|
|
106
|
+
* end in a **file extension**, and the clause must name **exactly one** — two
|
|
107
|
+
* paths are a conjunction this function cannot weigh, and `COMPOUND_RE` does
|
|
108
|
+
* not catch a comma.
|
|
109
|
+
*
|
|
110
|
+
* The extension requirement is what keeps a *directory* out. `agents/evidence/`
|
|
111
|
+
* exists in every checkout, so "recorded under `agents/evidence/`" is not a
|
|
112
|
+
* condition a probe can decide — and since `_exists` tests `isFile()`, matching
|
|
113
|
+
* it would have reported "does not exist" about a directory that does. A
|
|
114
|
+
* directory-shaped condition stays `undecidable`, which is the honest verdict.
|
|
115
|
+
*/
|
|
116
|
+
const REPO_PATH_RE = /`((?:agents|docs|src|tests|internal|scripts|evals|\.github)\/[A-Za-z0-9._\-/]*[A-Za-z0-9_\-]\.[A-Za-z0-9]{2,5})`/g;
|
|
87
117
|
/**
|
|
88
118
|
* A step reference: `P2.1`, `Phase 2.1`, or a bare `2.1`.
|
|
89
119
|
*
|
|
@@ -231,6 +261,37 @@ function _blankFencedCode(text: string): string {
|
|
|
231
261
|
);
|
|
232
262
|
}
|
|
233
263
|
|
|
264
|
+
/**
|
|
265
|
+
* The head of `s` up to its first sentence terminator OUTSIDE backticks.
|
|
266
|
+
*
|
|
267
|
+
* A naive character-class scan that stops at any dot cuts inside `foo.md` or
|
|
268
|
+
* inside a step id like `3.3`, which truncates the clause mid-span and hides
|
|
269
|
+
* the very path or step the condition names. Measured 2026-08-19: three of the six park notes written by
|
|
270
|
+
* `road-to-estate-drawdown` Phase 2 batch 1 lost their backticked path this
|
|
271
|
+
* way, and the probe then reported "names no roadmap this tree can resolve"
|
|
272
|
+
* about a condition that named a file.
|
|
273
|
+
*/
|
|
274
|
+
function _sentenceHead(s: string): string {
|
|
275
|
+
// An ODD backtick count means the span never closes, and toggling on it
|
|
276
|
+
// would leave `inCode` stuck true for the whole remainder — the commentary
|
|
277
|
+
// paragraph then becomes the clause, and a path mentioned only in that
|
|
278
|
+
// commentary could decide the verdict. Fall back to the plain scan, which
|
|
279
|
+
// cuts at the first terminator and is the conservative direction.
|
|
280
|
+
const balanced = (s.match(/`/g) ?? []).length % 2 === 0;
|
|
281
|
+
let inCode = false;
|
|
282
|
+
for (let i = 0; i < s.length; i++) {
|
|
283
|
+
const ch = s[i] as string;
|
|
284
|
+
if (ch === '`' && balanced) {
|
|
285
|
+
inCode = !inCode;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (!inCode && (ch === '.' || ch === '—' || ch === '–')) {
|
|
289
|
+
return s.slice(0, i);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return s;
|
|
293
|
+
}
|
|
294
|
+
|
|
234
295
|
/**
|
|
235
296
|
* The condition CLAUSE inside the condition text.
|
|
236
297
|
*
|
|
@@ -260,13 +321,63 @@ function conditionClause(condition: string): string {
|
|
|
260
321
|
// (`**Resume when** that pruning lands`), so take the span plus the
|
|
261
322
|
// remainder of its sentence — up to an em dash or a full stop.
|
|
262
323
|
const after = condition.slice(m.index + m[0].length);
|
|
263
|
-
|
|
264
|
-
return `${span}${tail}`;
|
|
324
|
+
return `${span}${_sentenceHead(after)}`;
|
|
265
325
|
}
|
|
266
326
|
}
|
|
267
327
|
return condition;
|
|
268
328
|
}
|
|
269
329
|
|
|
330
|
+
/**
|
|
331
|
+
* The clause's predicate is EXISTENCE, not some property of the file's content.
|
|
332
|
+
*
|
|
333
|
+
* Without this the path branch would answer a question the condition did not
|
|
334
|
+
* ask. Measured 2026-08-19 while writing it: `road-to-catalogue-host-fit` waits
|
|
335
|
+
* until `agents/evidence/metrics/skill-catalogue.jsonl` *holds at least 20
|
|
336
|
+
* observations*; the file exists today with 7 lines, so an existence test would
|
|
337
|
+
* have reported FIRED and un-parked a roadmap whose bar is 13 observations away.
|
|
338
|
+
* A content bar is decidable — by `capture_skill_catalogue --cadence` — but not
|
|
339
|
+
* by this probe, and `undecidable` is the honest verdict for it.
|
|
340
|
+
*
|
|
341
|
+
* The predicate is bound POSITIONALLY to the path rather than tested as a word
|
|
342
|
+
* anywhere in the clause, because word-presence is not a predicate: `\`x.md\` no
|
|
343
|
+
* longer exists` would have reported FIRED while the file was still there, and
|
|
344
|
+
* `blocked until a workaround exists, see \`docs/x.md\`` would have paired the
|
|
345
|
+
* word with a path it does not govern. Both shapes are regression tests.
|
|
346
|
+
*
|
|
347
|
+
* KNOWN LIMIT, stated rather than papered over: this probe reads the condition
|
|
348
|
+
* AFTER `_truncateAtNextField`, so a content bar an author moves into a sibling
|
|
349
|
+
* bolded field is invisible to it and the note decides as bare existence. That
|
|
350
|
+
* is an authoring bypass no check here can see — the guard is that a resume
|
|
351
|
+
* condition states its whole bar inside the condition, and it is model-carried.
|
|
352
|
+
*/
|
|
353
|
+
const EXISTENCE_PREDICATE_RE = /`(?:agents|docs|src|tests|internal|scripts|evals|\.github)\/[A-Za-z0-9._\-/]+`[ \t]*(?:currently[ \t]+)?exists\b/i;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The single repo-relative path a condition names, or `null`.
|
|
357
|
+
*
|
|
358
|
+
* `null` for zero paths (nothing to test) and for two or more (a conjunction
|
|
359
|
+
* this probe cannot weigh — reporting FIRED on the first would be the
|
|
360
|
+
* partial-match resume the compound guard already refuses).
|
|
361
|
+
*/
|
|
362
|
+
function referencedPath(condition: string): string | null {
|
|
363
|
+
if (!EXISTENCE_PREDICATE_RE.test(condition)) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const paths = _pathsIn(condition);
|
|
367
|
+
return paths.length === 1 ? (paths[0] as string) : null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Every distinct repo-relative file path a clause names, predicate or not. */
|
|
371
|
+
function _pathsIn(condition: string): string[] {
|
|
372
|
+
REPO_PATH_RE.lastIndex = 0;
|
|
373
|
+
const found = new Set<string>();
|
|
374
|
+
let m: RegExpExecArray | null;
|
|
375
|
+
while ((m = REPO_PATH_RE.exec(condition)) !== null) {
|
|
376
|
+
found.add(m[1] as string);
|
|
377
|
+
}
|
|
378
|
+
return [...found].sort();
|
|
379
|
+
}
|
|
380
|
+
|
|
270
381
|
/** Roadmap slugs named by the condition, minus the parked file's own slug. */
|
|
271
382
|
function referencedRoadmaps(condition: string, ownSlug: string): string[] {
|
|
272
383
|
const out = new Set<string>();
|
|
@@ -310,7 +421,10 @@ function stepIsDone(text: string, stepId: string): boolean | null {
|
|
|
310
421
|
* tree rather than the live estate — a probe whose only test is the live tree
|
|
311
422
|
* stops testing anything the day the live tree changes.
|
|
312
423
|
*/
|
|
313
|
-
function probeLater(roadmapRoot: string): ResumeFinding[] {
|
|
424
|
+
function probeLater(roadmapRoot: string, repoRootArg?: string): ResumeFinding[] {
|
|
425
|
+
// Defaults to the repo that owns `agents/roadmaps/`. Passed explicitly by
|
|
426
|
+
// the tests, whose fixture tree IS the roadmap root and has no repo above it.
|
|
427
|
+
const repoRoot = repoRootArg ?? path.resolve(roadmapRoot, '..', '..');
|
|
314
428
|
const laterDir = path.join(roadmapRoot, 'later');
|
|
315
429
|
let entries: string[];
|
|
316
430
|
try {
|
|
@@ -353,13 +467,43 @@ function probeLater(roadmapRoot: string): ResumeFinding[] {
|
|
|
353
467
|
continue;
|
|
354
468
|
}
|
|
355
469
|
const refs = referencedRoadmaps(clause, ownSlug);
|
|
470
|
+
// A clause naming a roadmap AND a path is a conjunction, and deciding it
|
|
471
|
+
// on the roadmap half alone is the partial-match resume `COMPOUND_RE`
|
|
472
|
+
// already refuses for two roadmaps. `COMPOUND_RE` does not close it —
|
|
473
|
+
// a comma joins the two without an `and`, which is the same hole the
|
|
474
|
+
// two-paths rule in `referencedPath` exists for. Refused symmetrically.
|
|
475
|
+
if (refs.length > 0 && _pathsIn(clause).length > 0) {
|
|
476
|
+
findings.push({
|
|
477
|
+
file: `later/${name}`,
|
|
478
|
+
condition,
|
|
479
|
+
refs,
|
|
480
|
+
verdict: 'undecidable',
|
|
481
|
+
why: 'names a roadmap and a repo path — a conjunction the probe can only half-weigh',
|
|
482
|
+
});
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
356
485
|
if (refs.length === 0) {
|
|
486
|
+
// Second decidable form: a single repo-relative path. `exists` is
|
|
487
|
+
// the whole test — the condition says the artefact will appear, so
|
|
488
|
+
// its appearance IS the firing, and no content check is implied.
|
|
489
|
+
const rel = referencedPath(clause);
|
|
490
|
+
if (rel !== null) {
|
|
491
|
+
const there = _exists(path.join(repoRoot, rel));
|
|
492
|
+
findings.push({
|
|
493
|
+
file: `later/${name}`,
|
|
494
|
+
condition,
|
|
495
|
+
refs,
|
|
496
|
+
verdict: there ? 'fired' : 'unmet',
|
|
497
|
+
why: there ? `${rel} exists` : `${rel} does not exist`,
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
357
501
|
findings.push({
|
|
358
502
|
file: `later/${name}`,
|
|
359
503
|
condition,
|
|
360
504
|
refs,
|
|
361
505
|
verdict: 'undecidable',
|
|
362
|
-
why: 'the condition names no roadmap this tree can resolve',
|
|
506
|
+
why: 'the condition names no roadmap or repo path this tree can resolve',
|
|
363
507
|
});
|
|
364
508
|
continue;
|
|
365
509
|
}
|
|
@@ -414,6 +558,7 @@ export {
|
|
|
414
558
|
conditionClause,
|
|
415
559
|
extractCondition,
|
|
416
560
|
probeLater,
|
|
561
|
+
referencedPath,
|
|
417
562
|
referencedRoadmaps,
|
|
418
563
|
roadmapDisposition,
|
|
419
564
|
stepIsDone,
|
|
@@ -342,6 +342,201 @@ function render(
|
|
|
342
342
|
* Returns '' when nothing is owned by the user, so the caller can append the
|
|
343
343
|
* result unconditionally and get silence when silence is correct.
|
|
344
344
|
*/
|
|
345
|
+
/**
|
|
346
|
+
* Whether a recorded recommendation was drafted by the agent rather than decided
|
|
347
|
+
* by the maintainer.
|
|
348
|
+
*
|
|
349
|
+
* Read from the field's own text, because that is where the claim has to live: a
|
|
350
|
+
* drafted recommendation is written INTO the roadmap so every consumer
|
|
351
|
+
* (`gates`, `--reply`, the sheet) sees the same string, and a marker held only
|
|
352
|
+
* in the sheet would be invisible to the other two. The convention is a leading
|
|
353
|
+
* `(agent-drafted …)` — matched loosely on purpose, since the parenthetical also
|
|
354
|
+
* carries a date and a sentence of provenance.
|
|
355
|
+
*/
|
|
356
|
+
export function isAgentDrafted(recommendation: string): boolean {
|
|
357
|
+
return /\(agent-drafted\b/i.test(recommendation);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* How a row names its decision.
|
|
362
|
+
*
|
|
363
|
+
* `legacy` is the parser's PLACEHOLDER id for a `> Blocked until …` note, not an
|
|
364
|
+
* identifier anyone wrote — `renderEntry` already substitutes "blocked-until
|
|
365
|
+
* note" for exactly that reason, and the sheet printing the raw placeholder made
|
|
366
|
+
* the two views disagree about the same row.
|
|
367
|
+
*/
|
|
368
|
+
export function sheetLabel(b: Blocker): string {
|
|
369
|
+
return isLegacy(b) ? 'blocked-until note' : `\`${b.id}\``;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** First sentence of a passage, for the one-line `Default:` column. */
|
|
373
|
+
function firstSentence(text: string): string {
|
|
374
|
+
const trimmed = text.trim();
|
|
375
|
+
if (trimmed === '') {
|
|
376
|
+
return '';
|
|
377
|
+
}
|
|
378
|
+
const m = /^(.+?[.!?])(\s|$)/.exec(trimmed.replace(/\s+/g, ' '));
|
|
379
|
+
return m === null ? trimmed.replace(/\s+/g, ' ') : (m[1] as string);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The one-line question a sheet row leads with.
|
|
384
|
+
*
|
|
385
|
+
* `Question:` is an optional field and only 5 of the 21 user-owned entries carry
|
|
386
|
+
* one (measured 2026-08-18), so a sheet that printed it and nothing else would
|
|
387
|
+
* be blank for three quarters of the estate. Falling back to the first step of
|
|
388
|
+
* `What to do:` and then to `Blocks:` is deterministic and derived from the
|
|
389
|
+
* blocker's own text — which is why the row LABELS which of the three it used
|
|
390
|
+
* rather than presenting all three as the same kind of statement.
|
|
391
|
+
*/
|
|
392
|
+
export function sheetQuestion(b: Blocker): { text: string; source: 'question' | 'todo' | 'blocks' } {
|
|
393
|
+
if (b.question.trim() !== '') {
|
|
394
|
+
return { text: firstSentence(b.question), source: 'question' };
|
|
395
|
+
}
|
|
396
|
+
const steps = regroupTodo(b.todo);
|
|
397
|
+
if (steps.length > 0 && (steps[0] as string).trim() !== '') {
|
|
398
|
+
return { text: firstSentence(steps[0] as string), source: 'todo' };
|
|
399
|
+
}
|
|
400
|
+
return { text: firstSentence(b.blocks), source: 'blocks' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The consolidated decision sheet — every user-owned decision in ONE artefact.
|
|
405
|
+
*
|
|
406
|
+
* `road-to-estate-drawdown` Phase 0 exists because thirteen reading assignments
|
|
407
|
+
* spread across thirteen files did not happen once. The sheet is the whole of
|
|
408
|
+
* the human's contribution to that campaign, front-loaded: one document, sorted
|
|
409
|
+
* by how much each answer unblocks, with a default per item so that
|
|
410
|
+
* accept-all-defaults is a valid answer.
|
|
411
|
+
*
|
|
412
|
+
* This differs from `--all` in the two ways that matter for that job. `--all`
|
|
413
|
+
* renders every blocker grouped by owner and is a terminal view; the sheet is
|
|
414
|
+
* user-owned ONLY, and every row carries a `Default:` plus the PROVENANCE of
|
|
415
|
+
* that default. Provenance is the load-bearing column: a sheet that presented an
|
|
416
|
+
* agent-drafted default beside a maintainer-recorded one as the same thing would
|
|
417
|
+
* invite accept-all over exactly the items nobody has examined.
|
|
418
|
+
*
|
|
419
|
+
* No population figure is quoted here on purpose. The first version cited
|
|
420
|
+
* "14 of 21 record a `Recommendation:`, 7 do not" as the justification, and the
|
|
421
|
+
* change that added this renderer then drafted six of those seven — so the
|
|
422
|
+
* docstring was contradicted by the sheet committed beside it (R2 finding).
|
|
423
|
+
* The split is rendered in the sheet's own header from the live tree, which is
|
|
424
|
+
* the only place it cannot go stale.
|
|
425
|
+
*
|
|
426
|
+
* Deterministic by construction — the generator never invents a default. Where
|
|
427
|
+
* none is recorded it says so and names what the reader must supply, so
|
|
428
|
+
* regenerating the sheet cannot silently overwrite an answer with a guess.
|
|
429
|
+
*/
|
|
430
|
+
function renderSheet(entries: readonly Entry[], now: Date): string {
|
|
431
|
+
const mine = entries.filter((e) => needsUser(e.blocker.owner));
|
|
432
|
+
const stamp = now.toISOString().slice(0, 10);
|
|
433
|
+
if (mine.length === 0) {
|
|
434
|
+
return (
|
|
435
|
+
`# Consolidated decision sheet — nothing owned by you (${stamp})\n\n` +
|
|
436
|
+
'No open blocker in the active roadmap tree carries `Owner: user`. Nothing to answer.\n'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const noRec = mine.filter((e) => e.blocker.recommendation.trim() === '');
|
|
441
|
+
const legacyNotes = noRec.filter((e) => isLegacy(e.blocker)).length;
|
|
442
|
+
const missing = noRec.length - legacyNotes;
|
|
443
|
+
const drafted = mine.filter((e) => isAgentDrafted(e.blocker.recommendation)).length;
|
|
444
|
+
const maintainerRecorded = mine.length - noRec.length - drafted;
|
|
445
|
+
const out: string[] = [
|
|
446
|
+
`# Consolidated decision sheet — ${String(mine.length)} decisions owned by you`,
|
|
447
|
+
'',
|
|
448
|
+
`> Generated ${stamp} by \`agent-config gates --sheet\` over \`agents/roadmaps/\`.`,
|
|
449
|
+
'> Sorted by unblock count, descending. **Accept-all-defaults is a valid answer**',
|
|
450
|
+
'> (`road-to-estate-drawdown` blocker `b-consolidated-decision-sheet`, option (a));',
|
|
451
|
+
'> answering only the two largest-unblock items and deferring the rest is option (c),',
|
|
452
|
+
'> which the roadmap itself recommends. Whichever you pick, the agent writes the answers',
|
|
453
|
+
'> back into each roadmap at its own blocker — that is not your work.',
|
|
454
|
+
'>',
|
|
455
|
+
`> **Provenance of the ${String(mine.length)} defaults: ${String(maintainerRecorded)} maintainer-recorded ·`,
|
|
456
|
+
`> ${String(drafted)} \`agent-drafted\` · ${String(missing)} with no recommendation at all ·`,
|
|
457
|
+
`> ${String(legacyNotes)} legacy \`> Blocked until …\` note(s) that have no field to carry one.**`,
|
|
458
|
+
'> The distinction is in every row on purpose: an agent-drafted default is the',
|
|
459
|
+
'> least-examined thing on this sheet, and accept-all-defaults would accept those too.',
|
|
460
|
+
'>',
|
|
461
|
+
'> This file is DERIVED — every line above and below comes from the roadmaps themselves,',
|
|
462
|
+
'> so regenerating is deterministic and an answer written into this file would be lost.',
|
|
463
|
+
'> Answers go back into each roadmap at its own blocker; the agent does that.',
|
|
464
|
+
'',
|
|
465
|
+
'| # | Decision | Roadmap | Unblocks | Default source |',
|
|
466
|
+
'|---:|---|---|---:|---|',
|
|
467
|
+
];
|
|
468
|
+
mine.forEach((e, i) => {
|
|
469
|
+
const source =
|
|
470
|
+
e.blocker.recommendation.trim() !== ''
|
|
471
|
+
? isAgentDrafted(e.blocker.recommendation)
|
|
472
|
+
? '`agent-drafted`'
|
|
473
|
+
: 'maintainer-recorded'
|
|
474
|
+
: isLegacy(e.blocker)
|
|
475
|
+
? 'none — legacy note'
|
|
476
|
+
: 'none recorded';
|
|
477
|
+
out.push(
|
|
478
|
+
`| ${String(i + 1)} | ${sheetLabel(e.blocker)} | ${e.roadmapRel} | ` +
|
|
479
|
+
`${String(e.openSteps)} | ${source} |`,
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
mine.forEach((e, i) => {
|
|
484
|
+
const q = sheetQuestion(e.blocker);
|
|
485
|
+
const qLabel =
|
|
486
|
+
q.source === 'question'
|
|
487
|
+
? 'recorded `Question:`'
|
|
488
|
+
: q.source === 'todo'
|
|
489
|
+
? 'derived from the first `What to do:` step'
|
|
490
|
+
: 'derived from `Blocks:`';
|
|
491
|
+
const recorded = e.blocker.recommendation.trim();
|
|
492
|
+
out.push(
|
|
493
|
+
'',
|
|
494
|
+
`## ${String(i + 1)} · ${sheetLabel(e.blocker)}`,
|
|
495
|
+
'',
|
|
496
|
+
`- **Roadmap:** ${e.roadmapRel}`,
|
|
497
|
+
`- **Unblocks:** ${String(e.openSteps)} open step(s) — ${e.blocker.blocks || '(not recorded)'}`,
|
|
498
|
+
`- **Question** (${qLabel})**:** ${q.text || '(the blocker records no question, no steps and no Blocks: field)'}`,
|
|
499
|
+
);
|
|
500
|
+
if (recorded === '' && isLegacy(e.blocker)) {
|
|
501
|
+
// A legacy `> Blocked until …` note is not a `### blocker:` entry and
|
|
502
|
+
// has no field to carry a recommendation. Saying "agent-drafted, to
|
|
503
|
+
// be written in" would point at a slot that does not exist, so the
|
|
504
|
+
// row names the actual next action instead.
|
|
505
|
+
out.push(
|
|
506
|
+
'- **Default:** _this is a legacy `> Blocked until …` note, not a `### blocker:` entry,' +
|
|
507
|
+
' so it has no `Recommendation:` field to read. Converting it into a real blocker' +
|
|
508
|
+
' entry is what gives it a default._',
|
|
509
|
+
'- **Default source:** none — legacy note',
|
|
510
|
+
);
|
|
511
|
+
} else if (recorded === '') {
|
|
512
|
+
out.push(
|
|
513
|
+
'- **Default:** _none recorded — needs an agent-drafted default before this row can be' +
|
|
514
|
+
' answered by accepting it._',
|
|
515
|
+
'- **Default source:** `agent-drafted` (to be written in below, and marked as such)',
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
const drafted = isAgentDrafted(recorded);
|
|
519
|
+
out.push(
|
|
520
|
+
`- **Default:** ${firstSentence(recorded)}`,
|
|
521
|
+
drafted
|
|
522
|
+
? '- **Default source:** `agent-drafted` — written into the roadmap’s ' +
|
|
523
|
+
'`Recommendation:` field and marked there, NOT a maintainer decision'
|
|
524
|
+
: '- **Default source:** maintainer-recorded `Recommendation:` in the roadmap',
|
|
525
|
+
`- **Recommendation (full):** ${recorded.replace(/\s+/g, ' ')}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (e.blocker.ifNothing.trim() !== '') {
|
|
529
|
+
out.push(`- **If you do nothing:** ${e.blocker.ifNothing.replace(/\s+/g, ' ')}`);
|
|
530
|
+
}
|
|
531
|
+
out.push(
|
|
532
|
+
`- **Done when:** ${e.blocker.resolvedWhen.replace(/\s+/g, ' ') || '(not recorded)'}`,
|
|
533
|
+
'- **Your answer:** _(accept default · override · defer)_',
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
out.push('');
|
|
537
|
+
return out.join('\n');
|
|
538
|
+
}
|
|
539
|
+
|
|
345
540
|
function renderReply(entries: readonly Entry[]): string {
|
|
346
541
|
const mine = entries.filter((e) => needsUser(e.blocker.owner));
|
|
347
542
|
if (mine.length === 0) {
|
|
@@ -564,6 +759,7 @@ function main(argv?: readonly string[]): number {
|
|
|
564
759
|
const json = args.includes('--json');
|
|
565
760
|
const reply = args.includes('--reply');
|
|
566
761
|
const pending = args.includes('--pending');
|
|
762
|
+
const sheet = args.includes('--sheet');
|
|
567
763
|
|
|
568
764
|
// `--execute <id>` is answered FIRST, and it is the only mode that writes.
|
|
569
765
|
// It used to sit after the `--pending` early return, so `--pending
|
|
@@ -643,13 +839,20 @@ function main(argv?: readonly string[]): number {
|
|
|
643
839
|
// `--reply` deliberately does not carry the probe: ADR-222 fixes that form
|
|
644
840
|
// at exactly ONE decision rendered in full, and a fired resume condition is
|
|
645
841
|
// not a decision the reader owes anybody — it is a file that can move.
|
|
646
|
-
|
|
842
|
+
// `--reply` and `--sheet` deliberately do not carry the probe: ADR-222 fixes
|
|
843
|
+
// the reply form at exactly ONE decision rendered in full, and a fired resume
|
|
844
|
+
// condition is not a decision the reader owes anybody — it is a file that can
|
|
845
|
+
// move. The sheet is the same argument for the same reason: it is the set of
|
|
846
|
+
// answers the user owes, and a movable file is not one of them.
|
|
847
|
+
const resumed = reply || sheet ? [] : probeLater(roadmapRoot, repoRoot);
|
|
647
848
|
process.stdout.write(
|
|
648
|
-
|
|
649
|
-
?
|
|
650
|
-
:
|
|
651
|
-
?
|
|
652
|
-
:
|
|
849
|
+
sheet
|
|
850
|
+
? renderSheet(entries, new Date())
|
|
851
|
+
: reply
|
|
852
|
+
? renderReply(entries)
|
|
853
|
+
: json
|
|
854
|
+
? renderJson(entries, all, resumed)
|
|
855
|
+
: render(entries, all, resumed),
|
|
653
856
|
);
|
|
654
857
|
return 0;
|
|
655
858
|
}
|
|
@@ -687,6 +890,7 @@ export {
|
|
|
687
890
|
renderJson,
|
|
688
891
|
renderReply,
|
|
689
892
|
renderResumed,
|
|
893
|
+
renderSheet,
|
|
690
894
|
renderPending,
|
|
691
895
|
renderPendingJson,
|
|
692
896
|
};
|