@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/brief/cli.d.ts +30 -0
- package/dist/brief/cli.d.ts.map +1 -0
- package/dist/brief/cli.js +85 -0
- package/dist/brief/collect.d.ts +17 -0
- package/dist/brief/collect.d.ts.map +1 -0
- package/dist/brief/collect.js +302 -0
- package/dist/brief/config.d.ts +40 -0
- package/dist/brief/config.d.ts.map +1 -0
- package/dist/brief/config.js +85 -0
- package/dist/brief/constants.d.ts +10 -0
- package/dist/brief/constants.d.ts.map +1 -0
- package/dist/brief/constants.js +9 -0
- package/dist/brief/docs.d.ts +53 -0
- package/dist/brief/docs.d.ts.map +1 -0
- package/dist/brief/docs.js +229 -0
- package/dist/brief/freshness.d.ts +104 -0
- package/dist/brief/freshness.d.ts.map +1 -0
- package/dist/brief/freshness.js +263 -0
- package/dist/brief/git-signals.d.ts +17 -0
- package/dist/brief/git-signals.d.ts.map +1 -0
- package/dist/brief/git-signals.js +101 -0
- package/dist/brief/index.d.ts +11 -0
- package/dist/brief/index.d.ts.map +1 -0
- package/dist/brief/index.js +5 -0
- package/dist/brief/reconcile.d.ts +75 -0
- package/dist/brief/reconcile.d.ts.map +1 -0
- package/dist/brief/reconcile.js +259 -0
- package/dist/brief/render.d.ts +60 -0
- package/dist/brief/render.d.ts.map +1 -0
- package/dist/brief/render.js +407 -0
- package/dist/brief/sanitize.d.ts +38 -0
- package/dist/brief/sanitize.d.ts.map +1 -0
- package/dist/brief/sanitize.js +48 -0
- package/dist/brief/selectors.d.ts +36 -0
- package/dist/brief/selectors.d.ts.map +1 -0
- package/dist/brief/selectors.js +75 -0
- package/dist/brief/story.d.ts +18 -0
- package/dist/brief/story.d.ts.map +1 -0
- package/dist/brief/story.js +59 -0
- package/dist/brief/types.d.ts +110 -0
- package/dist/brief/types.d.ts.map +1 -0
- package/dist/brief/types.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +166 -0
- package/dist/cli/init-args.d.ts +16 -0
- package/dist/cli/init-args.d.ts.map +1 -0
- package/dist/cli/init-args.js +24 -0
- package/dist/cli/ui-args.d.ts +23 -0
- package/dist/cli/ui-args.d.ts.map +1 -0
- package/dist/cli/ui-args.js +66 -0
- package/dist/cli/ui.d.ts +40 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +180 -0
- package/dist/coordinatorium/checkpoint.d.ts +44 -0
- package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
- package/dist/coordinatorium/checkpoint.js +124 -0
- package/dist/coordinatorium/cli.d.ts +39 -0
- package/dist/coordinatorium/cli.d.ts.map +1 -0
- package/dist/coordinatorium/cli.js +203 -0
- package/dist/coordinatorium/errors.d.ts +112 -0
- package/dist/coordinatorium/errors.d.ts.map +1 -0
- package/dist/coordinatorium/errors.js +145 -0
- package/dist/coordinatorium/index.d.ts +11 -0
- package/dist/coordinatorium/index.d.ts.map +1 -0
- package/dist/coordinatorium/index.js +5 -0
- package/dist/coordinatorium/lock.d.ts +187 -0
- package/dist/coordinatorium/lock.d.ts.map +1 -0
- package/dist/coordinatorium/lock.js +659 -0
- package/dist/coordinatorium/mission.d.ts +32 -0
- package/dist/coordinatorium/mission.d.ts.map +1 -0
- package/dist/coordinatorium/mission.js +87 -0
- package/dist/coordinatorium/state-keeper.d.ts +115 -0
- package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
- package/dist/coordinatorium/state-keeper.js +396 -0
- package/dist/coordinatorium/types.d.ts +69 -0
- package/dist/coordinatorium/types.d.ts.map +1 -0
- package/dist/coordinatorium/types.js +27 -0
- package/dist/durable/assess.d.ts +105 -0
- package/dist/durable/assess.d.ts.map +1 -0
- package/dist/durable/assess.js +239 -0
- package/dist/durable/canonical.d.ts +25 -0
- package/dist/durable/canonical.d.ts.map +1 -0
- package/dist/durable/canonical.js +78 -0
- package/dist/durable/cli.d.ts +37 -0
- package/dist/durable/cli.d.ts.map +1 -0
- package/dist/durable/cli.js +141 -0
- package/dist/durable/errors.d.ts +188 -0
- package/dist/durable/errors.d.ts.map +1 -0
- package/dist/durable/errors.js +317 -0
- package/dist/durable/fs-store.d.ts +233 -0
- package/dist/durable/fs-store.d.ts.map +1 -0
- package/dist/durable/fs-store.js +1393 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.d.ts.map +1 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/log.d.ts +33 -0
- package/dist/durable/log.d.ts.map +1 -0
- package/dist/durable/log.js +157 -0
- package/dist/durable/memory-store.d.ts +51 -0
- package/dist/durable/memory-store.d.ts.map +1 -0
- package/dist/durable/memory-store.js +302 -0
- package/dist/durable/projection.d.ts +35 -0
- package/dist/durable/projection.d.ts.map +1 -0
- package/dist/durable/projection.js +328 -0
- package/dist/durable/prune-check.d.ts +17 -0
- package/dist/durable/prune-check.d.ts.map +1 -0
- package/dist/durable/prune-check.js +45 -0
- package/dist/durable/register.d.ts +100 -0
- package/dist/durable/register.d.ts.map +1 -0
- package/dist/durable/register.js +532 -0
- package/dist/durable/retention.d.ts +49 -0
- package/dist/durable/retention.d.ts.map +1 -0
- package/dist/durable/retention.js +59 -0
- package/dist/durable/runtime.d.ts +497 -0
- package/dist/durable/runtime.d.ts.map +1 -0
- package/dist/durable/runtime.js +1271 -0
- package/dist/durable/seams.d.ts +40 -0
- package/dist/durable/seams.d.ts.map +1 -0
- package/dist/durable/seams.js +21 -0
- package/dist/durable/testing.d.ts +11 -0
- package/dist/durable/testing.d.ts.map +1 -0
- package/dist/durable/testing.js +25 -0
- package/dist/durable/types.d.ts +442 -0
- package/dist/durable/types.d.ts.map +1 -0
- package/dist/durable/types.js +75 -0
- package/dist/graph/builder.d.ts +11 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +153 -0
- package/dist/graph/identity.d.ts +37 -0
- package/dist/graph/identity.d.ts.map +1 -0
- package/dist/graph/identity.js +46 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +3 -0
- package/dist/graph/resolver.d.ts +41 -0
- package/dist/graph/resolver.d.ts.map +1 -0
- package/dist/graph/resolver.js +36 -0
- package/dist/graph/types.d.ts +42 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/server.d.ts +76 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +195 -0
- package/dist/http/ui-assets.d.ts +22 -0
- package/dist/http/ui-assets.d.ts.map +1 -0
- package/dist/http/ui-assets.js +100 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/init/index.d.ts +12 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +90 -0
- package/dist/init/prompts.d.ts +3 -0
- package/dist/init/prompts.d.ts.map +1 -0
- package/dist/init/prompts.js +47 -0
- package/dist/init/scaffold.d.ts +25 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +204 -0
- package/dist/init/types.d.ts +14 -0
- package/dist/init/types.d.ts.map +1 -0
- package/dist/init/types.js +1 -0
- package/dist/readmodel/dashboard.d.ts +40 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -0
- package/dist/readmodel/dashboard.js +236 -0
- package/dist/readmodel/index.d.ts +3 -0
- package/dist/readmodel/index.d.ts.map +1 -0
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/types.d.ts +104 -0
- package/dist/readmodel/types.d.ts.map +1 -0
- package/dist/readmodel/types.js +15 -0
- package/dist/root/index.d.ts +179 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.js +418 -0
- package/dist/root/safe-write.d.ts +268 -0
- package/dist/root/safe-write.d.ts.map +1 -0
- package/dist/root/safe-write.js +966 -0
- package/dist/root/write-artifacts.d.ts +20 -0
- package/dist/root/write-artifacts.d.ts.map +1 -0
- package/dist/root/write-artifacts.js +42 -0
- package/dist/scriptorium/compile.d.ts +4 -0
- package/dist/scriptorium/compile.d.ts.map +1 -0
- package/dist/scriptorium/compile.js +185 -0
- package/dist/scriptorium/frontmatter.d.ts +13 -0
- package/dist/scriptorium/frontmatter.d.ts.map +1 -0
- package/dist/scriptorium/frontmatter.js +90 -0
- package/dist/scriptorium/index.d.ts +3 -0
- package/dist/scriptorium/index.d.ts.map +1 -0
- package/dist/scriptorium/index.js +1 -0
- package/dist/scriptorium/markdown.d.ts +31 -0
- package/dist/scriptorium/markdown.d.ts.map +1 -0
- package/dist/scriptorium/markdown.js +88 -0
- package/dist/scriptorium/types.d.ts +78 -0
- package/dist/scriptorium/types.d.ts.map +1 -0
- package/dist/scriptorium/types.js +1 -0
- package/dist/types/enforcement.d.ts +11 -0
- package/dist/types/enforcement.d.ts.map +1 -0
- package/dist/types/enforcement.js +1 -0
- package/dist/types/integrity.d.ts +33 -0
- package/dist/types/integrity.d.ts.map +1 -0
- package/dist/types/integrity.js +1 -0
- package/dist/types/project-definition.d.ts +30 -0
- package/dist/types/project-definition.d.ts.map +1 -0
- package/dist/types/project-definition.js +1 -0
- package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
- package/dist/ui/assets/index-C4NhMaq7.js +28 -0
- package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
- package/dist/ui/index.html +13 -0
- package/dist/validate/index.d.ts +6 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +214 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +67 -4
- package/templates/.gitkeep +0 -0
- package/templates/default-instance/instruction/.cursorrules +53 -0
- package/templates/default-instance/instruction/.windsurfrules +53 -0
- package/templates/default-instance/instruction/CLAUDE.md +53 -0
- package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
- package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
- package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
- package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
- package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
- package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +147 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +73 -0
- package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
- package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
- package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
- package/templates/default-instance/monolith/governance/README.md +29 -0
- package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
- package/templates/default-instance/monolith/learning/INDEX.md +48 -0
- package/templates/default-instance/monolith/learning/injection-map.md +49 -0
- package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
- package/templates/default-instance/monolith/memory/errors/README.md +26 -0
- package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
- package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
- package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
- package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
- package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
- package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
- package/templates/default-instance/monolith/memory/working/active.md +41 -0
- package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
- package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
- package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
- package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
- package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
- package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
- package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
- package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
- package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
- package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
- package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
- package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
- package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
- package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
- package/templates/default-instance/monolith/skills/INDEX.md +28 -0
- package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
- package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
- package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
- package/templates/default-instance/monolith/state.json +11 -0
- package/templates/default-instance/monolith/tests/INDEX.md +57 -0
- package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
- package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
- package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
- package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
- package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
- package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
- package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
- package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
- package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
- package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
- package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
- package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
- package/templates/default-instance/projectroot/AGENTS.md +107 -0
- package/templates/default-instance/projectroot/monolith.config.json +32 -0
- package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render(model, maxChars)` — the Rendering stage of the brief pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Pure and synchronous: same BriefModel + same maxChars always produces the
|
|
5
|
+
* byte-identical string (section 11 — determinism). No Date.now(), no
|
|
6
|
+
* random IDs, no locale-dependent sorting; every list collect() hands in is
|
|
7
|
+
* already in a fixed, stably-sorted order.
|
|
8
|
+
*
|
|
9
|
+
* Budget accounting (section 10) in two phases to avoid a circular
|
|
10
|
+
* dependency between "does P0 fit" and "what does the Completeness line
|
|
11
|
+
* say" (rule: no diagnostic/marker line may be added without being counted
|
|
12
|
+
* in the budget that decided what else fits):
|
|
13
|
+
*
|
|
14
|
+
* Phase 1 — build all P0 content (identity, State Keeper status,
|
|
15
|
+
* collect-time diagnostics, every active mission identifier, every
|
|
16
|
+
* mandatory protocol pointer) with NO budget limit — P0 is never
|
|
17
|
+
* truncated or hidden (rule 5/7). If P0 alone (plus the reserved,
|
|
18
|
+
* fixed-size Completeness line) exceeds maxChars, that is reported via
|
|
19
|
+
* BUDGET_EXCEEDED_BY_REQUIRED (which also states explicitly that P1–P3
|
|
20
|
+
* are skipped entirely as a result — review finding F-5) and nothing
|
|
21
|
+
* else is attempted.
|
|
22
|
+
*
|
|
23
|
+
* Phase 2 — whatever chars remain after P0 + the reserved Completeness
|
|
24
|
+
* line are handed to P1 (Checkpoints, then Story — Checkpoints first so
|
|
25
|
+
* a long Story can never silently starve "Next steps" out of the budget;
|
|
26
|
+
* review finding F-5), then P2, then P3, in that fixed order. Each
|
|
27
|
+
* section either fits whole, or (list sections) includes as many whole
|
|
28
|
+
* items as fit plus an explicit "N more omitted" marker, or (the Story
|
|
29
|
+
* excerpt) is field-truncated with an explicit `[truncated N chars]`
|
|
30
|
+
* marker, or — if not even that fits — falls back to a short, fixed-size
|
|
31
|
+
* local omission marker so a section can never disappear from the output
|
|
32
|
+
* without some local trace of its omission (review finding F-5; the only
|
|
33
|
+
* residual case where a section can still vanish with zero local marker
|
|
34
|
+
* is when `remaining` is too small even for that minimal marker, which
|
|
35
|
+
* is itself already covered by the global Completeness: INCOMPLETE
|
|
36
|
+
* signal). Every one of these outcomes is itself measured and subtracted
|
|
37
|
+
* from the remaining budget before the next section is attempted —
|
|
38
|
+
* nothing is added "for free" after the fact.
|
|
39
|
+
*
|
|
40
|
+
* Completeness contract (N-3): `Completeness: COMPLETE` means no
|
|
41
|
+
* *substantive data* was lost, not that every section heading is present.
|
|
42
|
+
* An optional section that has nothing to show (an empty list, rendered
|
|
43
|
+
* only as "(none)") and the static P3 pointer block may be dropped under a
|
|
44
|
+
* tight budget without flipping the result to INCOMPLETE — dropping them
|
|
45
|
+
* loses no information. A section that holds data is never dropped
|
|
46
|
+
* silently: it is shown whole, shown partially with an explicit marker, or
|
|
47
|
+
* replaced by a pointer-bearing omission marker, and either of the latter
|
|
48
|
+
* two forces INCOMPLETE. P0 is never dropped. All project-controlled
|
|
49
|
+
* strings, provenance pointers included, pass through
|
|
50
|
+
* `sanitizeSingleLine` (N-1).
|
|
51
|
+
*/
|
|
52
|
+
import { ACTIVE_MD_REL } from './constants.js';
|
|
53
|
+
import { sanitizeSingleLine, sanitizeMultiLine } from './sanitize.js';
|
|
54
|
+
// Shorthand for the existing single-line sanitizer — applied to every string
|
|
55
|
+
// that originates in project files or configuration (N-1), including
|
|
56
|
+
// provenance pointers, which embed config-controlled paths.
|
|
57
|
+
const san = sanitizeSingleLine;
|
|
58
|
+
export const DEFAULT_MAX_CHARS = 6000;
|
|
59
|
+
const RECENT_FINISHED_MISSIONS_RENDER_LIMIT = 5;
|
|
60
|
+
const COMPLETE_LINE = 'Completeness: COMPLETE';
|
|
61
|
+
const INCOMPLETE_LINE = 'Completeness: INCOMPLETE';
|
|
62
|
+
const RESERVED_COMPLETENESS_LEN = Math.max(COMPLETE_LINE.length, INCOMPLETE_LINE.length) + 1;
|
|
63
|
+
const BUDGET_EXCEEDED_LINE = 'BUDGET_EXCEEDED_BY_REQUIRED: required (P0) content exceeds --max-chars; shown in full regardless ' +
|
|
64
|
+
'(--max-chars is not honored in this case). Optional sections (Current Story, Active Mission ' +
|
|
65
|
+
'Checkpoints, Accepted ADR Index, Recently Finished Missions) are skipped entirely as a result.';
|
|
66
|
+
function formatDiagnosticsBlock(diagnostics, extraLine) {
|
|
67
|
+
const lines = diagnostics.map((d) => ` [${d.severity.toUpperCase()}] ${d.code}: ${sanitizeSingleLine(d.message)}`);
|
|
68
|
+
if (extraLine)
|
|
69
|
+
lines.push(` [ERROR] ${extraLine}`);
|
|
70
|
+
const body = lines.length > 0 ? lines.join('\n') + '\n' : ' (none)\n';
|
|
71
|
+
return `\n## Diagnostics\n${body}`;
|
|
72
|
+
}
|
|
73
|
+
function formatStateKeeperBlock(sk) {
|
|
74
|
+
const versionNote = sk.schemaVersionOnDisk !== undefined ? ` schemaVersionOnDisk=${sk.schemaVersionOnDisk}` : '';
|
|
75
|
+
return `\n## State Keeper\n status: ${sk.status}${versionNote}\n (source: ${san(sk.provenance.source)})\n`;
|
|
76
|
+
}
|
|
77
|
+
function formatMission(m) {
|
|
78
|
+
return ` - ${san(m.id)} [${m.status}] ${san(m.title)} (${san(m.system)}) — started ${san(m.startedAt)}\n`;
|
|
79
|
+
}
|
|
80
|
+
function formatActiveMissionsBlock(missions, provenance) {
|
|
81
|
+
const body = missions.length > 0 ? missions.map(formatMission).join('') : ' (none)\n';
|
|
82
|
+
return `\n## Active Missions (${missions.length})\n${body} (source: ${san(provenance.source)})\n`;
|
|
83
|
+
}
|
|
84
|
+
function formatDocRef(d) {
|
|
85
|
+
return ` - ${san(d.id)}: ${san(d.title)} (${san(d.path)})\n`;
|
|
86
|
+
}
|
|
87
|
+
function formatProtocolsBlock(items, provenance) {
|
|
88
|
+
const body = items.length > 0 ? items.map(formatDocRef).join('') : ' (none)\n';
|
|
89
|
+
return `\n## Mandatory Protocols (${items.length})\n${body} (source: ${san(provenance.source)})\n`;
|
|
90
|
+
}
|
|
91
|
+
function formatIdentityTitle(model) {
|
|
92
|
+
return `\n# MONOLITH Brief — ${san(model.identity.name)}\n(monolithRoot: ${san(model.identity.monolithRoot)}, source: ${san(model.identity.provenance.source)})\n`;
|
|
93
|
+
}
|
|
94
|
+
const ACTIVE_STORY_LABEL = {
|
|
95
|
+
'none-recorded': 'none recorded (this does not mean the project is complete)',
|
|
96
|
+
recorded: 'recorded in active.md',
|
|
97
|
+
unknown: 'unknown',
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* SELF-AWARENESS-001: the Freshness block. Part of P0 — it is never truncated,
|
|
101
|
+
* and it never influences `Completeness`. Every string that originates in
|
|
102
|
+
* project files or git history passes through the sanitizer.
|
|
103
|
+
*/
|
|
104
|
+
function formatFreshnessBlock(f) {
|
|
105
|
+
const lines = ['', '## Freshness', ` Status: ${f.status}`];
|
|
106
|
+
if (f.lastStateUpdate) {
|
|
107
|
+
lines.push(` Last state update: ${san(f.lastStateUpdate.sha.slice(0, 7))} (${san(f.lastStateUpdate.date.slice(0, 10))}, git)`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(` Active Story: ${ACTIVE_STORY_LABEL[f.activeStory]}`);
|
|
110
|
+
if (f.reasons.length > 0) {
|
|
111
|
+
lines.push(' Evidence:');
|
|
112
|
+
for (const r of f.reasons)
|
|
113
|
+
lines.push(` - [${r.code}] ${san(r.message)}`);
|
|
114
|
+
}
|
|
115
|
+
if (f.warnings.length > 0) {
|
|
116
|
+
lines.push(' Warnings:');
|
|
117
|
+
for (const w of f.warnings)
|
|
118
|
+
lines.push(` - ${san(w)}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(` Recommendation: ${san(f.recommendation)}`);
|
|
121
|
+
return lines.join('\n') + '\n';
|
|
122
|
+
}
|
|
123
|
+
function buildCoreP0(model, includeBudgetExceeded) {
|
|
124
|
+
return (formatIdentityTitle(model) +
|
|
125
|
+
(model.freshness ? formatFreshnessBlock(model.freshness) : '') +
|
|
126
|
+
formatStateKeeperBlock(model.stateKeeper) +
|
|
127
|
+
formatDiagnosticsBlock(model.diagnostics, includeBudgetExceeded ? BUDGET_EXCEEDED_LINE : null) +
|
|
128
|
+
formatActiveMissionsBlock(model.missions.active, model.missions.provenance) +
|
|
129
|
+
formatProtocolsBlock(model.mandatoryProtocols.items, model.mandatoryProtocols.provenance));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Truncates `text` to fit within `maxBodyLen`, appending an explicit,
|
|
133
|
+
* pointer-bearing `[truncated N chars — see <pointer>]` marker where N is
|
|
134
|
+
* the actual number of characters removed.
|
|
135
|
+
*
|
|
136
|
+
* Review finding F-4: N depends on the marker's own length (removing more
|
|
137
|
+
* chars to make room for a longer "N" can itself change how many digits N
|
|
138
|
+
* has), so a single-pass `text.length - maxBodyLen` estimate under-reports
|
|
139
|
+
* N by exactly the marker's length. This resolves it as a small fixed-point
|
|
140
|
+
* computation: rebuild the marker from the current estimate, re-derive how
|
|
141
|
+
* much is actually kept, and repeat until N stops changing (converges in at
|
|
142
|
+
* most a couple of iterations — the only way it can change at all is a
|
|
143
|
+
* digit-count crossing in N, which an 8-iteration cap comfortably covers).
|
|
144
|
+
*
|
|
145
|
+
* The cut point is also nudged left by one character if it would otherwise
|
|
146
|
+
* split a UTF-16 surrogate pair, and N is recomputed once more for that
|
|
147
|
+
* final cut point so it stays exact even in that case.
|
|
148
|
+
*/
|
|
149
|
+
function truncateToBudget(text, maxBodyLen, pointer) {
|
|
150
|
+
if (text.length <= maxBodyLen)
|
|
151
|
+
return text;
|
|
152
|
+
const buildMarker = (removed) => `\n[truncated ${removed} chars — see ${pointer}]`;
|
|
153
|
+
let removed = text.length - maxBodyLen;
|
|
154
|
+
let keepLen = 0;
|
|
155
|
+
for (let i = 0; i < 8; i++) {
|
|
156
|
+
const marker = buildMarker(removed);
|
|
157
|
+
keepLen = Math.max(0, maxBodyLen - marker.length);
|
|
158
|
+
const nextRemoved = text.length - keepLen;
|
|
159
|
+
if (nextRemoved === removed)
|
|
160
|
+
break;
|
|
161
|
+
removed = nextRemoved;
|
|
162
|
+
}
|
|
163
|
+
if (keepLen > 0 && keepLen < text.length) {
|
|
164
|
+
const code = text.charCodeAt(keepLen - 1);
|
|
165
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
166
|
+
keepLen -= 1; // don't split a surrogate pair
|
|
167
|
+
}
|
|
168
|
+
const finalRemoved = text.length - keepLen;
|
|
169
|
+
return text.slice(0, keepLen) + buildMarker(finalRemoved);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Absolute last-resort fallback (review finding F-5): a label- and
|
|
173
|
+
* pointer-free marker so short it fits almost any nonzero `remaining`
|
|
174
|
+
* budget. Each section tries its own more informative (labeled,
|
|
175
|
+
* pointer-bearing) fallback first; this is only reached when even that
|
|
176
|
+
* doesn't fit. The only way a section can still end up with zero local
|
|
177
|
+
* trace is `remaining` being smaller than this marker itself — by that
|
|
178
|
+
* point the budget is so exhausted that the global `Completeness:
|
|
179
|
+
* INCOMPLETE` line is the only thing that could fit anyway.
|
|
180
|
+
*/
|
|
181
|
+
const BARE_OMIT_MARKER = '\n[omitted — budget]\n';
|
|
182
|
+
function buildStorySection(model, remaining) {
|
|
183
|
+
const header = '\n## Current Story\n';
|
|
184
|
+
const pointer = san(model.story.provenance?.source ?? ACTIVE_MD_REL);
|
|
185
|
+
const minimalFallback = `\n[Story omitted — budget; see ${pointer}]\n`;
|
|
186
|
+
let body;
|
|
187
|
+
if (!model.story.fileExists) {
|
|
188
|
+
body = ' (active.md not found)\n';
|
|
189
|
+
}
|
|
190
|
+
else if (!model.story.hasCurrentStorySection) {
|
|
191
|
+
body = ' (no "Current Story" section found)\n';
|
|
192
|
+
}
|
|
193
|
+
else if (model.story.excerpt === '') {
|
|
194
|
+
body = ' (section present but empty)\n';
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
body = `${sanitizeMultiLine(model.story.excerpt ?? '')}\n`;
|
|
198
|
+
}
|
|
199
|
+
const provenanceLine = model.story.provenance ? ` (source: ${san(model.story.provenance.source)})\n` : '';
|
|
200
|
+
const full = header + body + provenanceLine;
|
|
201
|
+
if (full.length <= remaining) {
|
|
202
|
+
return { text: full, consumed: full.length, lossFlag: false };
|
|
203
|
+
}
|
|
204
|
+
if (model.story.excerpt) {
|
|
205
|
+
const sanitizedExcerpt = sanitizeMultiLine(model.story.excerpt);
|
|
206
|
+
const fixedOverhead = header.length + 1 + provenanceLine.length;
|
|
207
|
+
const budgetForBody = remaining - fixedOverhead;
|
|
208
|
+
if (budgetForBody > 0) {
|
|
209
|
+
const truncatedBody = truncateToBudget(sanitizedExcerpt, budgetForBody, pointer);
|
|
210
|
+
const truncatedFull = header + truncatedBody + '\n' + provenanceLine;
|
|
211
|
+
if (truncatedFull.length <= remaining) {
|
|
212
|
+
return { text: truncatedFull, consumed: truncatedFull.length, lossFlag: true };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const omitLine = `\n## Current Story\n [omitted — budget] see ${pointer}\n`;
|
|
217
|
+
if (omitLine.length <= remaining) {
|
|
218
|
+
return { text: omitLine, consumed: omitLine.length, lossFlag: true };
|
|
219
|
+
}
|
|
220
|
+
if (minimalFallback.length <= remaining) {
|
|
221
|
+
return { text: minimalFallback, consumed: minimalFallback.length, lossFlag: true };
|
|
222
|
+
}
|
|
223
|
+
if (BARE_OMIT_MARKER.length <= remaining) {
|
|
224
|
+
return { text: BARE_OMIT_MARKER, consumed: BARE_OMIT_MARKER.length, lossFlag: true };
|
|
225
|
+
}
|
|
226
|
+
return { text: '', consumed: 0, lossFlag: true };
|
|
227
|
+
}
|
|
228
|
+
function formatCheckpointItem(cp) {
|
|
229
|
+
const lines = [` - Mission ${san(cp.missionId)}:`];
|
|
230
|
+
if (!cp.latest) {
|
|
231
|
+
lines.push(' Next steps: UNKNOWN (no checkpoints)');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
lines.push(` checkpoint: ${san(cp.latest.id)} (createdAt ${san(cp.latest.createdAt)})`);
|
|
235
|
+
lines.push(` summary: ${san(cp.latest.summary)}`);
|
|
236
|
+
if (cp.latest.remainingSteps.length === 0) {
|
|
237
|
+
lines.push(' Next steps: none recorded');
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
lines.push(' Next steps:');
|
|
241
|
+
for (const step of cp.latest.remainingSteps)
|
|
242
|
+
lines.push(` - ${san(step)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push(` (source: ${san(cp.provenance.source)})`);
|
|
246
|
+
return lines.join('\n') + '\n';
|
|
247
|
+
}
|
|
248
|
+
function buildCheckpointsSection(model, remaining) {
|
|
249
|
+
if (model.checkpoints.length === 0)
|
|
250
|
+
return { text: '', consumed: 0, lossFlag: false };
|
|
251
|
+
const header = '\n## Active Mission Checkpoints\n';
|
|
252
|
+
const items = model.checkpoints.map(formatCheckpointItem);
|
|
253
|
+
const minimalFallback = '\n[Active Mission Checkpoints omitted — budget; run `monolith mission checkpoints <id>`]\n';
|
|
254
|
+
const makeMarker = (n) => ` ...and ${n} more mission(s)' checkpoint details omitted (budget) — run \`monolith mission checkpoints <id>\`.\n`;
|
|
255
|
+
let used = header.length;
|
|
256
|
+
const included = [];
|
|
257
|
+
for (const item of items) {
|
|
258
|
+
if (used + item.length <= remaining) {
|
|
259
|
+
included.push(item);
|
|
260
|
+
used += item.length;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let omitted = items.length - included.length;
|
|
267
|
+
let marker = '';
|
|
268
|
+
if (omitted > 0) {
|
|
269
|
+
marker = makeMarker(omitted);
|
|
270
|
+
while (included.length > 0 && used + marker.length > remaining) {
|
|
271
|
+
const removedItem = included.pop();
|
|
272
|
+
used -= removedItem.length;
|
|
273
|
+
omitted = items.length - included.length;
|
|
274
|
+
marker = makeMarker(omitted);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const fits = omitted === 0 || used + marker.length <= remaining;
|
|
278
|
+
if (fits) {
|
|
279
|
+
if (included.length === 0 && !marker)
|
|
280
|
+
return { text: '', consumed: 0, lossFlag: false };
|
|
281
|
+
const text = header + included.join('') + marker;
|
|
282
|
+
return { text, consumed: text.length, lossFlag: omitted > 0 };
|
|
283
|
+
}
|
|
284
|
+
if (minimalFallback.length <= remaining) {
|
|
285
|
+
return { text: minimalFallback, consumed: minimalFallback.length, lossFlag: true };
|
|
286
|
+
}
|
|
287
|
+
if (BARE_OMIT_MARKER.length <= remaining) {
|
|
288
|
+
return { text: BARE_OMIT_MARKER, consumed: BARE_OMIT_MARKER.length, lossFlag: true };
|
|
289
|
+
}
|
|
290
|
+
return { text: '', consumed: 0, lossFlag: true };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Generic fitter for a deterministically-ordered list of small, atomic text
|
|
294
|
+
* items: include as many whole items as fit, in order, then an explicit
|
|
295
|
+
* "N more omitted" marker for the rest, then a provenance footer (section 9
|
|
296
|
+
* — every content section names its source). Items are never cut mid-item;
|
|
297
|
+
* the footer is reserved up front so it is never silently dropped while
|
|
298
|
+
* items still fit. If not even the whole-items-plus-marker form fits, falls
|
|
299
|
+
* back to a short, fixed-size `minimalFallback` marker rather than
|
|
300
|
+
* disappearing without any local trace (review finding F-5).
|
|
301
|
+
*/
|
|
302
|
+
function fitItemList(header, items, footer, remaining, omittedMarkerFn, minimalFallback) {
|
|
303
|
+
const noneBody = ' (none)\n';
|
|
304
|
+
if (items.length === 0) {
|
|
305
|
+
const full = header + noneBody + footer;
|
|
306
|
+
if (full.length <= remaining)
|
|
307
|
+
return { text: full, consumed: full.length, lossFlag: false };
|
|
308
|
+
const withoutFooter = header + noneBody;
|
|
309
|
+
if (withoutFooter.length <= remaining)
|
|
310
|
+
return { text: withoutFooter, consumed: withoutFooter.length, lossFlag: false };
|
|
311
|
+
return { text: '', consumed: 0, lossFlag: false };
|
|
312
|
+
}
|
|
313
|
+
const itemBudget = Math.max(0, remaining - footer.length);
|
|
314
|
+
let used = header.length;
|
|
315
|
+
const included = [];
|
|
316
|
+
for (const item of items) {
|
|
317
|
+
if (used + item.length <= itemBudget) {
|
|
318
|
+
included.push(item);
|
|
319
|
+
used += item.length;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
let omitted = items.length - included.length;
|
|
326
|
+
let marker = '';
|
|
327
|
+
if (omitted > 0) {
|
|
328
|
+
marker = omittedMarkerFn(omitted);
|
|
329
|
+
while (included.length > 0 && used + marker.length > itemBudget) {
|
|
330
|
+
const removedItem = included.pop();
|
|
331
|
+
used -= removedItem.length;
|
|
332
|
+
omitted = items.length - included.length;
|
|
333
|
+
marker = omittedMarkerFn(omitted);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const fits = omitted === 0 || used + marker.length <= itemBudget;
|
|
337
|
+
if (fits) {
|
|
338
|
+
const withFooter = header + included.join('') + marker + footer;
|
|
339
|
+
if (withFooter.length <= remaining) {
|
|
340
|
+
return { text: withFooter, consumed: withFooter.length, lossFlag: omitted > 0 };
|
|
341
|
+
}
|
|
342
|
+
const withoutFooter = header + included.join('') + marker;
|
|
343
|
+
if (withoutFooter.length <= remaining) {
|
|
344
|
+
return { text: withoutFooter, consumed: withoutFooter.length, lossFlag: true };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (minimalFallback.length <= remaining) {
|
|
348
|
+
return { text: minimalFallback, consumed: minimalFallback.length, lossFlag: true };
|
|
349
|
+
}
|
|
350
|
+
if (BARE_OMIT_MARKER.length <= remaining) {
|
|
351
|
+
return { text: BARE_OMIT_MARKER, consumed: BARE_OMIT_MARKER.length, lossFlag: true };
|
|
352
|
+
}
|
|
353
|
+
return { text: '', consumed: 0, lossFlag: true };
|
|
354
|
+
}
|
|
355
|
+
const P3_TEXT = '\n## More\n' +
|
|
356
|
+
' Full checkpoint history: `monolith mission checkpoints <id>`\n' +
|
|
357
|
+
' Mission detail: `monolith mission show <id>`\n' +
|
|
358
|
+
' Full ADR/protocol listings: see their directories above.\n';
|
|
359
|
+
export function render(model, maxChars = DEFAULT_MAX_CHARS) {
|
|
360
|
+
const coreText0 = buildCoreP0(model, false);
|
|
361
|
+
// The Freshness headline sits right under Completeness and is counted in the budget like it.
|
|
362
|
+
const freshnessLine = model.freshness ? `Freshness: ${model.freshness.status}\n` : '';
|
|
363
|
+
const requiredMinimum = coreText0.length + RESERVED_COMPLETENESS_LEN + freshnessLine.length;
|
|
364
|
+
const budgetExceededByRequired = requiredMinimum > maxChars;
|
|
365
|
+
const coreText = budgetExceededByRequired ? buildCoreP0(model, true) : coreText0;
|
|
366
|
+
let remaining = budgetExceededByRequired ? 0 : Math.max(0, maxChars - coreText.length - RESERVED_COMPLETENESS_LEN - freshnessLine.length);
|
|
367
|
+
let budgetLoss = false;
|
|
368
|
+
let p1 = '';
|
|
369
|
+
let p2 = '';
|
|
370
|
+
let p3 = '';
|
|
371
|
+
if (!budgetExceededByRequired) {
|
|
372
|
+
// Checkpoints (carries "Next steps") is given first claim on the P1
|
|
373
|
+
// budget, ahead of Story — a long Story excerpt must not be able to
|
|
374
|
+
// silently starve Next-steps information out of the brief (review
|
|
375
|
+
// finding F-5).
|
|
376
|
+
const ckpt = buildCheckpointsSection(model, remaining);
|
|
377
|
+
p1 += ckpt.text;
|
|
378
|
+
remaining -= ckpt.consumed;
|
|
379
|
+
budgetLoss = budgetLoss || ckpt.lossFlag;
|
|
380
|
+
const story = buildStorySection(model, remaining);
|
|
381
|
+
p1 += story.text;
|
|
382
|
+
remaining -= story.consumed;
|
|
383
|
+
budgetLoss = budgetLoss || story.lossFlag;
|
|
384
|
+
const adrSource = san(model.adrs.provenance.source);
|
|
385
|
+
const missionsSource = san(model.missions.provenance.source);
|
|
386
|
+
const adrItems = model.adrs.items.map((a) => formatDocRef(a));
|
|
387
|
+
const adrSection = fitItemList(`\n## Accepted ADR Index (${model.adrs.items.length})\n`, adrItems, ` (source: ${adrSource})\n`, remaining, (n) => ` ...and ${n} more ADR(s) omitted (budget) — see ${adrSource}.\n`, `\n[Accepted ADR Index omitted — budget; see ${adrSource}]\n`);
|
|
388
|
+
p2 += adrSection.text;
|
|
389
|
+
remaining -= adrSection.consumed;
|
|
390
|
+
budgetLoss = budgetLoss || adrSection.lossFlag;
|
|
391
|
+
const recentFinished = model.missions.recentFinished.slice(0, RECENT_FINISHED_MISSIONS_RENDER_LIMIT);
|
|
392
|
+
const finishedItems = recentFinished.map(formatMission);
|
|
393
|
+
const finishedSection = fitItemList(`\n## Recently Finished Missions (${recentFinished.length})\n`, finishedItems, ` (source: ${missionsSource})\n`, remaining, (n) => ` ...and ${n} more omitted (budget) — see ${missionsSource}.\n`, `\n[Recently Finished Missions omitted — budget; see ${missionsSource}]\n`);
|
|
394
|
+
p2 += finishedSection.text;
|
|
395
|
+
remaining -= finishedSection.consumed;
|
|
396
|
+
budgetLoss = budgetLoss || finishedSection.lossFlag;
|
|
397
|
+
if (P3_TEXT.length <= remaining) {
|
|
398
|
+
p3 = P3_TEXT;
|
|
399
|
+
remaining -= p3.length;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const collectTimeError = model.diagnostics.some((d) => d.severity === 'error');
|
|
403
|
+
const completeness = !collectTimeError && !budgetExceededByRequired && !budgetLoss ? 'COMPLETE' : 'INCOMPLETE';
|
|
404
|
+
const completenessLine = completeness === 'COMPLETE' ? COMPLETE_LINE : INCOMPLETE_LINE;
|
|
405
|
+
const text = `${completenessLine}\n${freshnessLine}${coreText}${p1}${p2}${p3}`;
|
|
406
|
+
return { text: `${text.replace(/\n+$/, '')}\n`, completeness };
|
|
407
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review finding F-7 (data/instruction boundary): `monolith brief` output
|
|
3
|
+
* is AI-facing. Project data — a Mission title, a checkpoint summary, a
|
|
4
|
+
* Story excerpt, a document title from frontmatter — is free text that a
|
|
5
|
+
* contributor (or an attacker with ordinary write access to the project)
|
|
6
|
+
* fully controls. Without escaping, a crafted value containing real
|
|
7
|
+
* newlines could fake a brief-native line such as `Completeness: COMPLETE`
|
|
8
|
+
* or a `## Section` heading, making project data visually indistinguishable
|
|
9
|
+
* from the brief's own structure to a reader (human or AI) skimming the
|
|
10
|
+
* raw text.
|
|
11
|
+
*
|
|
12
|
+
* This is deterministic, non-LLM hygiene only — it does not attempt to
|
|
13
|
+
* detect or block prompt injection as a concept, and makes no claim that
|
|
14
|
+
* it does. It guarantees two narrower, checkable properties:
|
|
15
|
+
* - a field rendered as a single line can never introduce a newline
|
|
16
|
+
* (so it can never start what looks like a new brief line/section);
|
|
17
|
+
* - control characters (escape codes, NUL, etc.) are never written to
|
|
18
|
+
* stdout raw, in single-line or multi-line fields alike.
|
|
19
|
+
* The Story excerpt is the one legitimately multi-line field (it is a
|
|
20
|
+
* verbatim block from active.md, already bounded by its own header and
|
|
21
|
+
* `(source: ...)` footer) — its internal newlines are preserved, only
|
|
22
|
+
* other control characters are escaped.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* For fields rendered as a single logical line (mission title, checkpoint
|
|
26
|
+
* summary, a remaining-step entry, a doc title): collapses any real
|
|
27
|
+
* newline to a visible `\n` escape sequence and escapes other control
|
|
28
|
+
* characters, so the field can never break out of its line.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sanitizeSingleLine(s: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* For fields allowed to span multiple lines (the Story excerpt): keeps
|
|
33
|
+
* real newlines (that is the field's legitimate internal structure) but
|
|
34
|
+
* still escapes other control characters so nothing unprintable reaches
|
|
35
|
+
* stdout raw.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sanitizeMultiLine(s: string): string;
|
|
38
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/brief/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAWH;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review finding F-7 (data/instruction boundary): `monolith brief` output
|
|
3
|
+
* is AI-facing. Project data — a Mission title, a checkpoint summary, a
|
|
4
|
+
* Story excerpt, a document title from frontmatter — is free text that a
|
|
5
|
+
* contributor (or an attacker with ordinary write access to the project)
|
|
6
|
+
* fully controls. Without escaping, a crafted value containing real
|
|
7
|
+
* newlines could fake a brief-native line such as `Completeness: COMPLETE`
|
|
8
|
+
* or a `## Section` heading, making project data visually indistinguishable
|
|
9
|
+
* from the brief's own structure to a reader (human or AI) skimming the
|
|
10
|
+
* raw text.
|
|
11
|
+
*
|
|
12
|
+
* This is deterministic, non-LLM hygiene only — it does not attempt to
|
|
13
|
+
* detect or block prompt injection as a concept, and makes no claim that
|
|
14
|
+
* it does. It guarantees two narrower, checkable properties:
|
|
15
|
+
* - a field rendered as a single line can never introduce a newline
|
|
16
|
+
* (so it can never start what looks like a new brief line/section);
|
|
17
|
+
* - control characters (escape codes, NUL, etc.) are never written to
|
|
18
|
+
* stdout raw, in single-line or multi-line fields alike.
|
|
19
|
+
* The Story excerpt is the one legitimately multi-line field (it is a
|
|
20
|
+
* verbatim block from active.md, already bounded by its own header and
|
|
21
|
+
* `(source: ...)` footer) — its internal newlines are preserved, only
|
|
22
|
+
* other control characters are escaped.
|
|
23
|
+
*/
|
|
24
|
+
// C0 controls other than \t (0x09), \n (0x0A), \r (0x0D), plus DEL (0x7F) and
|
|
25
|
+
// the C1 range (0x80-0x9F — includes 0x9B, a single-byte CSI that some
|
|
26
|
+
// terminals honor as an escape-sequence introducer, and 0x85, NEL).
|
|
27
|
+
const OTHER_CONTROL_CHARS_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
|
|
28
|
+
function escapeChar(ch) {
|
|
29
|
+
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* For fields rendered as a single logical line (mission title, checkpoint
|
|
33
|
+
* summary, a remaining-step entry, a doc title): collapses any real
|
|
34
|
+
* newline to a visible `\n` escape sequence and escapes other control
|
|
35
|
+
* characters, so the field can never break out of its line.
|
|
36
|
+
*/
|
|
37
|
+
export function sanitizeSingleLine(s) {
|
|
38
|
+
return s.replace(/\r\n|\r|\n/g, '\\n').replace(OTHER_CONTROL_CHARS_RE, escapeChar);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* For fields allowed to span multiple lines (the Story excerpt): keeps
|
|
42
|
+
* real newlines (that is the field's legitimate internal structure) but
|
|
43
|
+
* still escapes other control characters so nothing unprintable reaches
|
|
44
|
+
* stdout raw.
|
|
45
|
+
*/
|
|
46
|
+
export function sanitizeMultiLine(s) {
|
|
47
|
+
return s.replace(/\r\n/g, '\n').replace(OTHER_CONTROL_CHARS_RE, escapeChar);
|
|
48
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, read-only selectors over an already-loaded CoordinatoriumState.
|
|
3
|
+
*
|
|
4
|
+
* `listMissions()`/`listCheckpoints()` in mission.ts/checkpoint.ts each call
|
|
5
|
+
* `readState()` independently, which would mean re-reading state.json once
|
|
6
|
+
* per section of a brief. Collect-brief reads state.json exactly once (see
|
|
7
|
+
* collect.ts) and derives everything else from that single snapshot through
|
|
8
|
+
* these selectors instead.
|
|
9
|
+
*/
|
|
10
|
+
import type { CoordinatoriumState, Mission, Checkpoint } from '../coordinatorium/types.js';
|
|
11
|
+
/** Stable, locale-independent string comparator (never localeCompare — ADR determinism rule). */
|
|
12
|
+
export declare function compareStrings(a: string, b: string): number;
|
|
13
|
+
export declare function selectActiveMissions(state: CoordinatoriumState): Mission[];
|
|
14
|
+
/**
|
|
15
|
+
* Finished missions (completed/failed/cancelled), most recently updated
|
|
16
|
+
* first, capped to `limit`. Used for the P2 "last finished missions" slice.
|
|
17
|
+
*/
|
|
18
|
+
export declare function selectRecentFinishedMissions(state: CoordinatoriumState, limit: number): Mission[];
|
|
19
|
+
/**
|
|
20
|
+
* The checkpoint with the highest numeric ordinal in its id
|
|
21
|
+
* (`<missionId>-CKPT-NNNN`) — the monotonic per-mission sequence number,
|
|
22
|
+
* exactly as nextCheckpointId() in checkpoint.ts derives it — NOT the one
|
|
23
|
+
* with the latest `createdAt` (section 6 of the brief contract: ID order is
|
|
24
|
+
* authoritative, timestamps are not). Returns null if the mission has no
|
|
25
|
+
* checkpoints.
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectLatestCheckpoint(state: CoordinatoriumState, missionId: string): Checkpoint | null;
|
|
28
|
+
/**
|
|
29
|
+
* True if some other checkpoint for this mission has a strictly later
|
|
30
|
+
* `createdAt` than the one selectLatestCheckpoint() picked by ID ordinal —
|
|
31
|
+
* i.e. timestamps and ID order disagree about which checkpoint is "latest".
|
|
32
|
+
* Purely informational (CHECKPOINT_ORDER_MISMATCH), never changes which
|
|
33
|
+
* checkpoint is reported.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasCheckpointOrderMismatch(state: CoordinatoriumState, missionId: string, latest: Checkpoint): boolean;
|
|
36
|
+
//# sourceMappingURL=selectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../src/brief/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE3F,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,EAAE,CAI1E;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,CAKjG;AAWD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAWvG;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAMrH"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, read-only selectors over an already-loaded CoordinatoriumState.
|
|
3
|
+
*
|
|
4
|
+
* `listMissions()`/`listCheckpoints()` in mission.ts/checkpoint.ts each call
|
|
5
|
+
* `readState()` independently, which would mean re-reading state.json once
|
|
6
|
+
* per section of a brief. Collect-brief reads state.json exactly once (see
|
|
7
|
+
* collect.ts) and derives everything else from that single snapshot through
|
|
8
|
+
* these selectors instead.
|
|
9
|
+
*/
|
|
10
|
+
/** Stable, locale-independent string comparator (never localeCompare — ADR determinism rule). */
|
|
11
|
+
export function compareStrings(a, b) {
|
|
12
|
+
if (a < b)
|
|
13
|
+
return -1;
|
|
14
|
+
if (a > b)
|
|
15
|
+
return 1;
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
export function selectActiveMissions(state) {
|
|
19
|
+
return Object.values(state.tactical.missions)
|
|
20
|
+
.filter((m) => m.status === 'in_progress')
|
|
21
|
+
.sort((a, b) => compareStrings(a.startedAt, b.startedAt) || compareStrings(a.id, b.id));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Finished missions (completed/failed/cancelled), most recently updated
|
|
25
|
+
* first, capped to `limit`. Used for the P2 "last finished missions" slice.
|
|
26
|
+
*/
|
|
27
|
+
export function selectRecentFinishedMissions(state, limit) {
|
|
28
|
+
return Object.values(state.tactical.missions)
|
|
29
|
+
.filter((m) => m.status !== 'in_progress')
|
|
30
|
+
.sort((a, b) => compareStrings(b.updatedAt, a.updatedAt) || compareStrings(b.id, a.id))
|
|
31
|
+
.slice(0, limit);
|
|
32
|
+
}
|
|
33
|
+
function escapeRegExp(s) {
|
|
34
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
}
|
|
36
|
+
/** All checkpoints belonging to one mission, in no particular order. */
|
|
37
|
+
function checkpointsForMission(state, missionId) {
|
|
38
|
+
return Object.values(state.tactical.checkpoints).filter((c) => c.missionId === missionId);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The checkpoint with the highest numeric ordinal in its id
|
|
42
|
+
* (`<missionId>-CKPT-NNNN`) — the monotonic per-mission sequence number,
|
|
43
|
+
* exactly as nextCheckpointId() in checkpoint.ts derives it — NOT the one
|
|
44
|
+
* with the latest `createdAt` (section 6 of the brief contract: ID order is
|
|
45
|
+
* authoritative, timestamps are not). Returns null if the mission has no
|
|
46
|
+
* checkpoints.
|
|
47
|
+
*/
|
|
48
|
+
export function selectLatestCheckpoint(state, missionId) {
|
|
49
|
+
const re = new RegExp(`^${escapeRegExp(missionId)}-CKPT-(\\d+)$`);
|
|
50
|
+
let best = null;
|
|
51
|
+
for (const c of checkpointsForMission(state, missionId)) {
|
|
52
|
+
const match = re.exec(c.id);
|
|
53
|
+
const ordinal = match ? Number(match[1]) : -1;
|
|
54
|
+
if (!best || ordinal > best.ordinal || (ordinal === best.ordinal && compareStrings(c.id, best.checkpoint.id) < 0)) {
|
|
55
|
+
best = { checkpoint: c, ordinal };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return best?.checkpoint ?? null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* True if some other checkpoint for this mission has a strictly later
|
|
62
|
+
* `createdAt` than the one selectLatestCheckpoint() picked by ID ordinal —
|
|
63
|
+
* i.e. timestamps and ID order disagree about which checkpoint is "latest".
|
|
64
|
+
* Purely informational (CHECKPOINT_ORDER_MISMATCH), never changes which
|
|
65
|
+
* checkpoint is reported.
|
|
66
|
+
*/
|
|
67
|
+
export function hasCheckpointOrderMismatch(state, missionId, latest) {
|
|
68
|
+
for (const c of checkpointsForMission(state, missionId)) {
|
|
69
|
+
if (c.id === latest.id)
|
|
70
|
+
continue;
|
|
71
|
+
if (compareStrings(c.createdAt, latest.createdAt) > 0)
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the "## Current Story" section from
|
|
3
|
+
* `.monolith/memory/working/active.md` — confirmed structure only, no
|
|
4
|
+
* heuristic Markdown interpretation (section 5 of the brief contract).
|
|
5
|
+
*
|
|
6
|
+
* Only two things are recognized: a line that is exactly a level-1..6
|
|
7
|
+
* heading, and a line that is exactly `---` (used in active.md as a visual
|
|
8
|
+
* section separator). The "Current Story" section's content is everything
|
|
9
|
+
* between its own heading line and the next such boundary.
|
|
10
|
+
*/
|
|
11
|
+
export interface StoryExtraction {
|
|
12
|
+
hasCurrentStorySection: boolean;
|
|
13
|
+
excerpt: string | null;
|
|
14
|
+
/** 1-indexed inclusive line range, e.g. "24-29". */
|
|
15
|
+
lineRange: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function extractCurrentStory(content: string): StoryExtraction;
|
|
18
|
+
//# sourceMappingURL=story.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/brief/story.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,eAAe;IAC9B,sBAAsB,EAAE,OAAO,CAAC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAoDpE"}
|