@latysh-development/monolith 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +24 -1
  2. package/dist/brief/collect.d.ts.map +1 -1
  3. package/dist/brief/collect.js +6 -2
  4. package/dist/brief/freshness.d.ts +33 -5
  5. package/dist/brief/freshness.d.ts.map +1 -1
  6. package/dist/brief/freshness.js +67 -13
  7. package/dist/brief/markdown-context.d.ts +22 -0
  8. package/dist/brief/markdown-context.d.ts.map +1 -0
  9. package/dist/brief/markdown-context.js +66 -0
  10. package/dist/brief/story.d.ts +2 -10
  11. package/dist/brief/story.d.ts.map +1 -1
  12. package/dist/brief/story.js +21 -7
  13. package/dist/cli/index.js +12 -0
  14. package/dist/finalize/cli.d.ts +10 -0
  15. package/dist/finalize/cli.d.ts.map +1 -0
  16. package/dist/finalize/cli.js +134 -0
  17. package/dist/finalize/core.d.ts +110 -0
  18. package/dist/finalize/core.d.ts.map +1 -0
  19. package/dist/finalize/core.js +773 -0
  20. package/dist/finalize/index.d.ts +5 -0
  21. package/dist/finalize/index.d.ts.map +1 -0
  22. package/dist/finalize/index.js +2 -0
  23. package/dist/finalize/proposal.d.ts +97 -0
  24. package/dist/finalize/proposal.d.ts.map +1 -0
  25. package/dist/finalize/proposal.js +213 -0
  26. package/dist/ui/apple-touch-icon.png +0 -0
  27. package/dist/ui/assets/index-5zXAgig8.css +1 -0
  28. package/dist/ui/assets/{index-C4NhMaq7.js → index-CQgoMYLM.js} +5 -5
  29. package/dist/ui/assets/monolith-mark-Ct0ammrU.webp +0 -0
  30. package/dist/ui/favicon-16x16.png +0 -0
  31. package/dist/ui/favicon-32x32.png +0 -0
  32. package/dist/ui/favicon.ico +0 -0
  33. package/dist/ui/index.html +6 -2
  34. package/dist/update/cli.d.ts +1 -1
  35. package/dist/update/cli.d.ts.map +1 -1
  36. package/dist/update/cli.js +10 -3
  37. package/dist/update/index.d.ts +9 -0
  38. package/dist/update/index.d.ts.map +1 -1
  39. package/dist/update/index.js +50 -1
  40. package/dist/update/legacy-skills.d.ts +15 -0
  41. package/dist/update/legacy-skills.d.ts.map +1 -0
  42. package/dist/update/legacy-skills.js +20 -0
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
  46. package/templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md +183 -0
  47. package/templates/default-instance/monolith/memory/working/active.md +11 -0
  48. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +68 -2
  49. package/dist/ui/assets/index-DMZ7ZvZY.css +0 -1
package/README.md CHANGED
@@ -101,6 +101,29 @@ monolith ui # Mission Control at http://127.0.0.1:4173 ; Ctrl+C to stop
101
101
  instance, and it does not open a browser — open the printed address yourself.
102
102
  All commands: [CLI reference](./docs/reference/cli.md).
103
103
 
104
+ ### 🧠 One More Thing: Give Your AI Chat the Context!
105
+
106
+ MONOLITH is installed. Now make sure your AI assistant knows how to use it.
107
+
108
+ Open `.monolith/context/MONOLITH-CHAT-CONTEXT.md` and copy its contents into your ChatGPT Project instructions, Claude conversation, or another AI workspace you use to coordinate development.
109
+
110
+ It explains MONOLITH's capabilities, project memory, workflows, and limitations.
111
+
112
+ **Your coding agent gets the protocols. Your working chat gets the context.**
113
+
114
+ `monolith init` creates this file. Print it from the project root (the directory where you ran `monolith init`):
115
+
116
+ ```bash
117
+ cat .monolith/context/MONOLITH-CHAT-CONTEXT.md
118
+ ```
119
+
120
+ If `monolithRoot` in `monolith.config.json` is not `.monolith`, use that directory instead.
121
+ Projects initialized with an earlier MONOLITH version do not get this file from
122
+ `monolith update`; copy it from the template in this repository (replace
123
+ `{{MONOLITH_ROOT}}` with `.monolith`, otherwise `monolith validate` reports an
124
+ unreplaced placeholder). You can also read the template before installing:
125
+ [`templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md`](./templates/default-instance/monolith/context/MONOLITH-CHAT-CONTEXT.md).
126
+
104
127
  ## Updating an Existing Project
105
128
 
106
129
  If a project was initialised with an earlier MONOLITH version, bring it up to the current agent integration (agent entry point and Idea Capture) with:
@@ -160,7 +183,7 @@ listing — not an official partnership, endorsement or certification.
160
183
 
161
184
  ## Project Status
162
185
 
163
- **Alpha** (`0.1.0-alpha.1`). The API and CLI are experimental and may change
186
+ **Alpha** (`0.1.0-alpha.3`). The API and CLI are experimental and may change
164
187
  without notice. Published to npm under the alpha dist-tag. Durable State and the Coordinatorium
165
188
  (`monolith durable`, `monolith mission`) are experimental and are not part of
166
189
  the Quick Start.
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAiBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AA8TnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/brief/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAyBH,OAAO,KAAK,EACV,UAAU,EAOX,MAAM,YAAY,CAAC;AAiBpB,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,QAAa,CAAC;AAiUnD,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqEpE"}
@@ -16,6 +16,7 @@ import { readBriefConfig } from './config.js';
16
16
  import { resolveMonolithRootValue, checkContainedFile, readBoundedFileStable, isReadSecurityViolation, findWriteArtifacts, describeWriteArtifacts, } from '../root/index.js';
17
17
  import { discoverDocs } from './docs.js';
18
18
  import { extractCurrentStory } from './story.js';
19
+ import { maskNonContent } from './markdown-context.js';
19
20
  import { selectActiveMissions, selectRecentFinishedMissions, selectLatestCheckpoint, hasCheckpointOrderMismatch, } from './selectors.js';
20
21
  import { stateFilePath, validateState, createInitialState, MAX_STATE_BYTES } from '../coordinatorium/index.js';
21
22
  import { ACTIVE_MD_REL, PARKED_IDEAS_LIMIT, PROTOCOLS_DIR, ROADMAP_INDEX_REL, STORY_INDEX_REL, HANDOFF_REL } from './constants.js';
@@ -202,7 +203,9 @@ function collectStory(targetDir, monolithRoot, root, diagnostics) {
202
203
  diagnostics.push({
203
204
  code: 'ACTIVE_MD_NO_CURRENT_STORY',
204
205
  severity: 'info',
205
- message: `${relPath} exists but has no "## Current Story" section.`,
206
+ message: extraction.ambiguous
207
+ ? `${relPath} has more than one "## Current Story" section outside code blocks and comments; the section is ambiguous and was not read.`
208
+ : `${relPath} exists but has no "## Current Story" section.`,
206
209
  });
207
210
  }
208
211
  return {
@@ -273,7 +276,8 @@ function collectFreshness(targetDir, monolithRoot, root, story) {
273
276
  roadmapActiveStory: roadmap === null ? null : parseRoadmapActiveStory(roadmap),
274
277
  roadmapStatuses: roadmap === null ? new Map() : parseRoadmapStatuses(roadmap),
275
278
  storyIndexStatuses: storyIndex === null ? new Map() : parseStoryIndexStatuses(storyIndex),
276
- recordedText: [...content.values()].filter((c) => c !== null).join('\n'),
279
+ // Ids shown only inside code fences or HTML comments are examples, not records.
280
+ recordedText: [...content.values()].filter((c) => c !== null).map(maskNonContent).join('\n'),
277
281
  stateFiles: new Set(stateFiles),
278
282
  };
279
283
  return { result: evaluateFreshness(evidence), evidence };
@@ -19,6 +19,7 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { maskNonContent } from './markdown-context.js';
22
23
  export type FreshnessStatus = 'FRESH' | 'STALE' | 'UNKNOWN';
23
24
  export type FreshnessReasonCode = 'STATE_CONTRADICTION' | 'UNRECORDED_WORK' | 'GIT_UNAVAILABLE' | 'STATE_NOT_IN_GIT' | 'GIT_HISTORY_TRUNCATED' | 'UNATTRIBUTED_ACTIVITY' | 'NO_ACTIVITY_SINCE_STATE_UPDATE' | 'ACTIVITY_RECORDED';
24
25
  export interface FreshnessReason {
@@ -57,8 +58,14 @@ export type StoryClaim = {
57
58
  } | {
58
59
  kind: 'ids';
59
60
  ids: string[];
60
- } | {
61
+ }
62
+ /**
63
+ * `contradiction`: the section both says "no active story" and has a
64
+ * structural `**Story:**` field (PSC-004A) — the field value(s), verbatim.
65
+ */
66
+ | {
61
67
  kind: 'unknown';
68
+ contradiction?: string[];
62
69
  };
63
70
  export interface FreshnessEvidence {
64
71
  git: GitSignals;
@@ -92,13 +99,34 @@ export interface FreshnessResult {
92
99
  }
93
100
  export declare function extractWorkItemIds(text: string): string[];
94
101
  export declare function mentionsId(text: string, id: string): boolean;
95
- /** Interprets the `## Current Story` excerpt of active.md. */
102
+ export { maskNonContent };
103
+ /**
104
+ * Interprets the `## Current Story` excerpt of active.md.
105
+ *
106
+ * Identity comes from the structural `**Story:** <id> …` field only (first
107
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
108
+ * or comments are never identity. Zero usable, several or malformed fields →
109
+ * `unknown` (never a guess, never STALE on its own).
110
+ *
111
+ * Compatibility: a historical section with no `**Story:**` field at all is
112
+ * still read when its first line is a *bold declaration* of exactly one
113
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
114
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
115
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
116
+ * indistinguishable from a sentence that merely mentions a story, so it is
117
+ * `unknown`, never a confirmed identity.
118
+ *
119
+ * "No active story" as the first visible line is `none` only when the section
120
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
121
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
122
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
123
+ */
96
124
  export declare function parseActiveStoryClaim(excerpt: string | null): StoryClaim;
97
- /** `Active Story: <text>` line of roadmap-index.md. */
125
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
98
126
  export declare function parseRoadmapActiveStory(content: string): string | null;
99
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
127
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
100
128
  export declare function parseRoadmapStatuses(content: string): Map<string, string>;
101
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
129
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
102
130
  export declare function parseStoryIndexStatuses(content: string): Map<string, string>;
103
131
  export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
104
132
  //# sourceMappingURL=freshness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,8 @@
19
19
  * absence of an active story alone never produce STALE, and the
20
20
  * absence of an active story never means "project complete".
21
21
  */
22
+ import { isIdeaId } from '../ideas/format.js';
23
+ import { declaresNoActiveStory, maskNonContent } from './markdown-context.js';
22
24
  // ---------------------------------------------------------------------------
23
25
  // Work-item ids
24
26
  // ---------------------------------------------------------------------------
@@ -30,8 +32,14 @@
30
32
  const WORK_ITEM_SOURCE = String.raw `(?:[A-Z]{2,}-)+\d{3}[A-Z]?(?:\.\d+)*(?:-[A-Z])?`;
31
33
  /** Decision / lesson records are not work items. */
32
34
  const NON_WORK_PREFIXES = ['ADR-', 'DEC-', 'LESSON-'];
35
+ /**
36
+ * Ideas live in their own namespace (`IDEA-NNN`, src/ideas/format.ts) and are
37
+ * stored in the idea store, not in the state documents. Only the exact idea
38
+ * grammar is excluded: `IDEA-001A`, `IDEA-001B.1` or `IDEA-003-R` are story
39
+ * ids that merely share the prefix and stay work items.
40
+ */
33
41
  function isWorkItem(id) {
34
- return !NON_WORK_PREFIXES.some((p) => id.startsWith(p));
42
+ return !NON_WORK_PREFIXES.some((p) => id.startsWith(p)) && !isIdeaId(id);
35
43
  }
36
44
  export function extractWorkItemIds(text) {
37
45
  const re = new RegExp(`(?<![A-Za-z0-9-])${WORK_ITEM_SOURCE}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`, 'g');
@@ -46,29 +54,69 @@ export function mentionsId(text, id) {
46
54
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
55
  return new RegExp(`(?<![A-Za-z0-9-])${escaped}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`).test(text);
48
56
  }
57
+ // `maskNonContent` lives in ./markdown-context.ts (shared with story extraction
58
+ // and DOCSYNC) and is re-exported here for existing importers.
59
+ export { maskNonContent };
49
60
  // ---------------------------------------------------------------------------
50
61
  // Parsers for the state documents (confirmed structure only)
51
62
  // ---------------------------------------------------------------------------
52
63
  const NONE_RE = /^\W*(?:no active story|none|unknown)\b|^\s*[—-]\s*$/i;
53
- /** Interprets the `## Current Story` excerpt of active.md. */
64
+ /** Same id grammar as DOCSYNC's `activeStoryIdentity()`. */
65
+ const STORY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
66
+ /**
67
+ * Interprets the `## Current Story` excerpt of active.md.
68
+ *
69
+ * Identity comes from the structural `**Story:** <id> …` field only (first
70
+ * token = id), exactly as DOCSYNC reads it. Ids in titles, prose, code fences
71
+ * or comments are never identity. Zero usable, several or malformed fields →
72
+ * `unknown` (never a guess, never STALE on its own).
73
+ *
74
+ * Compatibility: a historical section with no `**Story:**` field at all is
75
+ * still read when its first line is a *bold declaration* of exactly one
76
+ * work-item id — `**WIDGET-001 — title**`, `**WIDGET-001**` or
77
+ * `**WIDGET-001** — status` (the form old active.md files used). An unbolded
78
+ * line (`STORY-001 was completed yesterday.`, `STORY-001 — done`) is
79
+ * indistinguishable from a sentence that merely mentions a story, so it is
80
+ * `unknown`, never a confirmed identity.
81
+ *
82
+ * "No active story" as the first visible line is `none` only when the section
83
+ * has no structural `**Story:**` field at all; with one (valid, duplicate or
84
+ * malformed) the section contradicts itself → `unknown` with `contradiction`
85
+ * (PSC-004A, same rule as DOCSYNC's `activeStoryIdentity()`).
86
+ */
54
87
  export function parseActiveStoryClaim(excerpt) {
55
88
  if (excerpt === null)
56
89
  return { kind: 'unknown' };
57
- const firstLine = excerpt.split('\n').find((l) => l.trim() !== '') ?? '';
58
- if (/^\W*no active story\b/i.test(firstLine))
59
- return { kind: 'none' };
60
- const ids = extractWorkItemIds(excerpt);
61
- return ids.length > 0 ? { kind: 'ids', ids } : { kind: 'unknown' };
90
+ const masked = maskNonContent(excerpt);
91
+ const firstLine = masked.split('\n').find((l) => l.trim() !== '') ?? '';
92
+ const fields = masked
93
+ .split('\n')
94
+ .map((l) => /^\*\*Story:\*\*[ \t]*(.*)$/.exec(l.replace(/\r$/, '')))
95
+ .filter((m) => m !== null)
96
+ .map((m) => m[1].trim());
97
+ if (declaresNoActiveStory(excerpt))
98
+ return fields.length === 0 ? { kind: 'none' } : { kind: 'unknown', contradiction: fields };
99
+ if (fields.length > 0) {
100
+ if (fields.length > 1)
101
+ return { kind: 'unknown' };
102
+ const id = /^(\S+)(?:\s|$)/.exec(fields[0])?.[1];
103
+ return id !== undefined && STORY_ID_RE.test(id) ? { kind: 'ids', ids: [id] } : { kind: 'unknown' };
104
+ }
105
+ const line = firstLine.trim();
106
+ const idEnd = String.raw `(?![A-Za-z0-9]|\.\d|-[A-Z0-9])`;
107
+ const declaration = new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\*\\*(?:\\s+[—–-](?:\\s.*)?)?$`).exec(line) ??
108
+ new RegExp(`^\\*\\*(${WORK_ITEM_SOURCE})${idEnd}\\s+[—–-]\\s.*\\*\\*$`).exec(line);
109
+ return declaration !== null && extractWorkItemIds(line).length === 1 ? { kind: 'ids', ids: [declaration[1]] } : { kind: 'unknown' };
62
110
  }
63
- /** `Active Story: <text>` line of roadmap-index.md. */
111
+ /** `Active Story: <text>` line of roadmap-index.md (outside code fences and comments). */
64
112
  export function parseRoadmapActiveStory(content) {
65
- const m = /^Active Story:\s*(.+?)\s*$/m.exec(content);
113
+ const m = /^Active Story:\s*(.+?)\s*$/m.exec(maskNonContent(content));
66
114
  return m ? m[1] : null;
67
115
  }
68
- /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
116
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md (outside code fences and comments). */
69
117
  export function parseRoadmapStatuses(content) {
70
118
  const out = new Map();
71
- for (const line of content.split('\n')) {
119
+ for (const line of maskNonContent(content).split('\n')) {
72
120
  const cells = line.split('|').map((c) => c.trim());
73
121
  // leading and trailing '|' produce empty first/last cells
74
122
  if (cells.length < 5 || cells[0] !== '' || cells[cells.length - 1] !== '')
@@ -82,10 +130,10 @@ export function parseRoadmapStatuses(content) {
82
130
  }
83
131
  return out;
84
132
  }
85
- /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
133
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md (outside code fences and comments). */
86
134
  export function parseStoryIndexStatuses(content) {
87
135
  const out = new Map();
88
- const lines = content.split('\n');
136
+ const lines = maskNonContent(content).split('\n');
89
137
  for (let i = 0; i < lines.length; i++) {
90
138
  const h = /^###\s+(?:STORY\s+)?([A-Za-z0-9][A-Za-z0-9.\-]*)\s+—/.exec(lines[i]);
91
139
  if (!h)
@@ -117,6 +165,12 @@ function isDone(status) {
117
165
  }
118
166
  function findContradictions(ev) {
119
167
  const out = [];
168
+ if (ev.activeStory.kind === 'unknown' && ev.activeStory.contradiction !== undefined) {
169
+ out.push({
170
+ message: `active.md records no active story, but its Current Story section also has a **Story:** field (${ev.activeStory.contradiction.map((f) => `"${f.slice(0, 40)}"`).join(', ')}).`,
171
+ documents: ['active.md'],
172
+ });
173
+ }
120
174
  if (ev.activeStory.kind === 'none' && ev.roadmapActiveStory !== null && !NONE_RE.test(ev.roadmapActiveStory)) {
121
175
  out.push({
122
176
  message: `active.md records no active story, but roadmap-index.md names "${ev.roadmapActiveStory}".`,
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Markdown context shared by the Brief, Freshness, Reconcile and DOCSYNC
3
+ * readers of the project state documents. Pure, deterministic, no I/O.
4
+ */
5
+ /**
6
+ * True when the first visible line of a Current Story excerpt (outside code
7
+ * fences and HTML comments) declares that there is no active story. Shared by
8
+ * the Brief and DOCSYNC (PSC-004A): such a declaration next to a structural
9
+ * `**Story:**` field is a contradiction, never a confirmed "none" and never
10
+ * a confirmed identity.
11
+ */
12
+ export declare function declaresNoActiveStory(excerpt: string): boolean;
13
+ /**
14
+ * Blanks every line (or part of a line) that sits inside a fenced code block
15
+ * or an HTML comment, keeping the line count. Fence grammar matches DOCSYNC's
16
+ * `activeStoryIdentity()`: a line that starts with ``` or ~~~ opens a fence,
17
+ * and only a line made of the same character, at least as long, closes it.
18
+ * An unterminated fence or comment hides the rest of the text — an example
19
+ * that cannot be delimited is never read as a claim.
20
+ */
21
+ export declare function maskNonContent(text: string): string;
22
+ //# sourceMappingURL=markdown-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-context.d.ts","sourceRoot":"","sources":["../../src/brief/markdown-context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuCnD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Markdown context shared by the Brief, Freshness, Reconcile and DOCSYNC
3
+ * readers of the project state documents. Pure, deterministic, no I/O.
4
+ */
5
+ /**
6
+ * True when the first visible line of a Current Story excerpt (outside code
7
+ * fences and HTML comments) declares that there is no active story. Shared by
8
+ * the Brief and DOCSYNC (PSC-004A): such a declaration next to a structural
9
+ * `**Story:**` field is a contradiction, never a confirmed "none" and never
10
+ * a confirmed identity.
11
+ */
12
+ export function declaresNoActiveStory(excerpt) {
13
+ const first = maskNonContent(excerpt.replace(/\r\n/g, '\n')).split('\n').find((l) => l.trim() !== '') ?? '';
14
+ return /^\W*no active story\b/i.test(first);
15
+ }
16
+ /**
17
+ * Blanks every line (or part of a line) that sits inside a fenced code block
18
+ * or an HTML comment, keeping the line count. Fence grammar matches DOCSYNC's
19
+ * `activeStoryIdentity()`: a line that starts with ``` or ~~~ opens a fence,
20
+ * and only a line made of the same character, at least as long, closes it.
21
+ * An unterminated fence or comment hides the rest of the text — an example
22
+ * that cannot be delimited is never read as a claim.
23
+ */
24
+ export function maskNonContent(text) {
25
+ let fence = null;
26
+ let comment = false;
27
+ const out = [];
28
+ for (const raw of text.split('\n')) {
29
+ const line = raw.trim();
30
+ const f = /^(`{3,}|~{3,})/.exec(line);
31
+ if (fence !== null) {
32
+ if (f !== null && f[1][0] === fence[0] && f[1].length >= fence.length && line === f[1])
33
+ fence = null;
34
+ out.push('');
35
+ continue;
36
+ }
37
+ if (!comment && f !== null) {
38
+ fence = f[1];
39
+ out.push('');
40
+ continue;
41
+ }
42
+ let kept = '';
43
+ let i = 0;
44
+ while (i < raw.length) {
45
+ if (comment) {
46
+ const end = raw.indexOf('-->', i);
47
+ if (end === -1)
48
+ break;
49
+ comment = false;
50
+ i = end + 3;
51
+ }
52
+ else {
53
+ const start = raw.indexOf('<!--', i);
54
+ if (start === -1) {
55
+ kept += raw.slice(i);
56
+ break;
57
+ }
58
+ kept += raw.slice(i, start);
59
+ comment = true;
60
+ i = start + 4;
61
+ }
62
+ }
63
+ out.push(kept);
64
+ }
65
+ return out.join('\n');
66
+ }
@@ -1,18 +1,10 @@
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
1
  export interface StoryExtraction {
12
2
  hasCurrentStorySection: boolean;
13
3
  excerpt: string | null;
14
4
  /** 1-indexed inclusive line range, e.g. "24-29". */
15
5
  lineRange: string | null;
6
+ /** More than one real "## Current Story" heading; no section is returned. */
7
+ ambiguous?: boolean;
16
8
  }
17
9
  export declare function extractCurrentStory(content: string): StoryExtraction;
18
10
  //# sourceMappingURL=story.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/brief/story.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,eAAe;IAC9B,sBAAsB,EAAE,OAAO,CAAC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CA0DpE"}
@@ -7,25 +7,39 @@
7
7
  * heading, and a line that is exactly `---` (used in active.md as a visual
8
8
  * section separator). The "Current Story" section's content is everything
9
9
  * between its own heading line and the next such boundary.
10
+ *
11
+ * Headings and separators are structural only outside fenced code blocks and
12
+ * HTML comments (PSC-004): a `## Current Story` shown inside a Markdown
13
+ * example is not the section. If more than one real heading exists the
14
+ * section is ambiguous and is NOT read (`ambiguous: true`, no section): no
15
+ * consumer may guess which one is the project's current story.
10
16
  */
17
+ import { maskNonContent } from './markdown-context.js';
11
18
  const CURRENT_STORY_HEADING_RE = /^##\s+Current Story\s*$/i;
12
19
  const HEADING_RE = /^#{1,6}\s+/;
13
20
  const RULE_RE = /^-{3,}\s*$/;
14
21
  export function extractCurrentStory(content) {
15
22
  const lines = content.split('\n');
16
- let headingLineIdx = -1;
23
+ // Same line count as `lines`; fenced / commented text is blank.
24
+ const structural = maskNonContent(content).split('\n');
25
+ const headings = [];
17
26
  for (let i = 0; i < lines.length; i++) {
18
- if (CURRENT_STORY_HEADING_RE.test(lines[i].trim())) {
19
- headingLineIdx = i;
20
- break;
21
- }
27
+ // The heading line must be untouched by masking: a line carrying (or
28
+ // opening) an HTML comment is not a heading, so no comment state can leak
29
+ // from it into the section body.
30
+ if (CURRENT_STORY_HEADING_RE.test(structural[i].trim()) && structural[i].trim() === lines[i].trim())
31
+ headings.push(i);
22
32
  }
23
- if (headingLineIdx === -1) {
33
+ if (headings.length === 0) {
24
34
  return { hasCurrentStorySection: false, excerpt: null, lineRange: null };
25
35
  }
36
+ if (headings.length > 1) {
37
+ return { hasCurrentStorySection: false, excerpt: null, lineRange: null, ambiguous: true };
38
+ }
39
+ const headingLineIdx = headings[0];
26
40
  let endIdx = lines.length;
27
41
  for (let i = headingLineIdx + 1; i < lines.length; i++) {
28
- const trimmed = lines[i].trim();
42
+ const trimmed = structural[i].trim();
29
43
  if (HEADING_RE.test(trimmed) || RULE_RE.test(trimmed)) {
30
44
  endIdx = i;
31
45
  break;
package/dist/cli/index.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * monolith brief — deterministic, bounded project context summary
13
13
  * monolith durable — inspect/verify/maintain Durable State task logs
14
14
  * monolith idea — capture and recall deferred ideas (add/list/show)
15
+ * monolith finalize — plan/apply a controlled story finalization (documentation sync)
15
16
  * monolith update — add missing Idea Capture infrastructure to an existing instance
16
17
  * monolith ui — launch Mission Control (read-only dashboard)
17
18
  */
@@ -42,6 +43,8 @@ Commands:
42
43
  list | show | assess | packet | verify | retention.
43
44
  idea Capture a deferred thought without leaving the current task, and
44
45
  recall it later: add | list | show. Plain Markdown files, no LLM.
46
+ finalize Close a story with checked preconditions and controlled documentation
47
+ updates: plan (read-only) | apply --approve <digest>.
45
48
  update Bring an existing instance up to Idea Capture & Recall. Preview by
46
49
  default (changes nothing). --apply creates MONOLITH-managed files
47
50
  and may add or refresh managed sections inside your existing
@@ -74,6 +77,10 @@ Idea usage:
74
77
  monolith idea list
75
78
  monolith idea show <id>
76
79
 
80
+ Finalize usage:
81
+ monolith finalize plan --proposal <file|-> [--json]
82
+ monolith finalize apply --proposal <file|-> --approve <digest> [--json]
83
+
77
84
  Update usage:
78
85
  monolith update [--apply]
79
86
 
@@ -169,6 +176,11 @@ async function main() {
169
176
  await runIdeaCommand(args.slice(1));
170
177
  break;
171
178
  }
179
+ case 'finalize': {
180
+ const { runFinalizeCommand } = await import('../finalize/cli.js');
181
+ process.exitCode = await runFinalizeCommand(args.slice(1));
182
+ break;
183
+ }
172
184
  case 'update': {
173
185
  const { runUpdate } = await import('../update/cli.js');
174
186
  process.exitCode = await runUpdate(args.slice(1));
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `monolith finalize plan|apply` (DOCSYNC-001, ADR-022).
3
+ *
4
+ * `plan` is read-only. `apply` writes only with `--approve <digest>` taken from
5
+ * a `plan` the owner reviewed. Exit codes: 0 ok / already finalized; 1 usage or
6
+ * unreadable proposal; 2 not ready / refused (nothing written); 3 incomplete
7
+ * (partially applied — see the output).
8
+ */
9
+ export declare function runFinalizeCommand(args: string[], targetDir?: string): Promise<number>;
10
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/finalize/cli.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAuFH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBnG"}
@@ -0,0 +1,134 @@
1
+ /* eslint-disable no-console */
2
+ /**
3
+ * `monolith finalize plan|apply` (DOCSYNC-001, ADR-022).
4
+ *
5
+ * `plan` is read-only. `apply` writes only with `--approve <digest>` taken from
6
+ * a `plan` the owner reviewed. Exit codes: 0 ok / already finalized; 1 usage or
7
+ * unreadable proposal; 2 not ready / refused (nothing written); 3 incomplete
8
+ * (partially applied — see the output).
9
+ */
10
+ import { lstatSync, readFileSync } from 'fs';
11
+ import { applyFinalization, planFinalization } from './core.js';
12
+ import { parseProposal } from './proposal.js';
13
+ const USAGE = `Usage:
14
+ monolith finalize plan --proposal <file|-> [--json]
15
+ monolith finalize apply --proposal <file|-> --approve <digest> [--json]`;
16
+ const MAX_PROPOSAL_BYTES = 4 * 1024 * 1024;
17
+ // eslint-disable-next-line no-control-regex
18
+ const printable = (s) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
19
+ function parseFlags(args, allowApprove) {
20
+ const f = { json: false };
21
+ for (let i = 0; i < args.length; i++) {
22
+ const a = args[i];
23
+ if (a === '--json')
24
+ f.json = true;
25
+ else if (a === '--proposal' || (allowApprove && a === '--approve')) {
26
+ const v = args[i + 1];
27
+ if (v === undefined || v.startsWith('--'))
28
+ throw new Error(`${a} needs a value.\n${USAGE}`);
29
+ const key = a === '--proposal' ? 'proposal' : 'approve';
30
+ if (f[key] !== undefined)
31
+ throw new Error(`${a} was given more than once.\n${USAGE}`);
32
+ f[key] = v;
33
+ i++;
34
+ }
35
+ else
36
+ throw new Error(`Unexpected argument ${JSON.stringify(printable(a))}.\n${USAGE}`);
37
+ }
38
+ if (f.proposal === undefined)
39
+ throw new Error(`--proposal is required.\n${USAGE}`);
40
+ if (allowApprove && f.approve === undefined)
41
+ throw new Error(`--approve <digest> is required: run \`monolith finalize plan\`, review it, then pass its digest.\n${USAGE}`);
42
+ return f;
43
+ }
44
+ async function readProposal(src) {
45
+ let raw;
46
+ if (src === '-') {
47
+ const chunks = [];
48
+ let n = 0;
49
+ for await (const c of process.stdin) {
50
+ n += c.length;
51
+ if (n > MAX_PROPOSAL_BYTES)
52
+ throw new Error('proposal on stdin is too large.');
53
+ chunks.push(c);
54
+ }
55
+ raw = Buffer.concat(chunks).toString('utf8');
56
+ }
57
+ else {
58
+ const st = lstatSync(src);
59
+ if (!st.isFile())
60
+ throw new Error(`${src} is not a regular file.`);
61
+ if (st.size > MAX_PROPOSAL_BYTES)
62
+ throw new Error(`${src} is too large.`);
63
+ raw = readFileSync(src, 'utf8');
64
+ }
65
+ let json;
66
+ try {
67
+ json = JSON.parse(raw);
68
+ }
69
+ catch (err) {
70
+ throw new Error(`proposal is not valid JSON: ${err.message}`);
71
+ }
72
+ const parsed = parseProposal(json);
73
+ if (!parsed.ok)
74
+ throw new Error(`invalid proposal:\n${parsed.errors.map((e) => ` - ${e}`).join('\n')}`);
75
+ return parsed.proposal;
76
+ }
77
+ function printPlan(pl) {
78
+ console.log(`Finalization plan for ${printable(pl.storyId)}: ${pl.status}${pl.partial ? ' (PARTIAL: an earlier run was interrupted; apply rolls it forward)' : ''}`);
79
+ console.log('\nPreconditions:');
80
+ for (const c of pl.checks)
81
+ console.log(` [${c.status}] ${c.id}: ${printable(c.detail)}\n source: ${printable(c.source)}`);
82
+ console.log('\nTargets (in write order, active.md last):');
83
+ for (const t of pl.targets)
84
+ console.log(` [${t.state}] ${t.id} → ${printable(t.path)}${t.state === 'done' ? '' : ` — ${printable(t.reason)}`}`);
85
+ for (const d of pl.diagnostics)
86
+ console.log(`\nNote: ${printable(d)}`);
87
+ if (pl.status === 'READY')
88
+ console.log(`\nDigest: ${pl.digest}\nOwner approval: monolith finalize apply --proposal <same file> --approve ${pl.digest}\nThe digest shows this plan is unchanged; it does not prove who approves it. Do not run apply unless the owner approved this exact plan.`);
89
+ }
90
+ function printApply(r) {
91
+ console.log(`Finalization of ${printable(r.storyId)}: ${r.status}`);
92
+ if (r.applied.length > 0)
93
+ console.log(` written: ${r.applied.join(', ')}`);
94
+ if (r.status !== 'FINALIZED' && r.pending.length > 0)
95
+ console.log(` not written: ${r.pending.join(', ')}`);
96
+ if (r.failed)
97
+ console.log(` failed: ${r.failed.target}${r.failed.publication ? ` [${r.failed.publication}]` : ''}: ${printable(r.failed.message)}`);
98
+ for (const d of r.diagnostics)
99
+ console.log(` ${printable(d)}`);
100
+ if (r.status === 'REFUSED')
101
+ for (const c of r.plan.checks.filter((x) => x.status !== 'PASS'))
102
+ console.log(` [${c.status}] ${c.id}: ${printable(c.detail)}`);
103
+ }
104
+ export async function runFinalizeCommand(args, targetDir = process.cwd()) {
105
+ const sub = args[0];
106
+ try {
107
+ if (sub === 'plan') {
108
+ const f = parseFlags(args.slice(1), false);
109
+ const pl = planFinalization(targetDir, await readProposal(f.proposal));
110
+ if (f.json)
111
+ console.log(JSON.stringify(pl, null, 2));
112
+ else
113
+ printPlan(pl);
114
+ return pl.status === 'NOT_READY' ? 2 : 0;
115
+ }
116
+ if (sub === 'apply') {
117
+ const f = parseFlags(args.slice(1), true);
118
+ const r = await applyFinalization(targetDir, await readProposal(f.proposal), f.approve, {
119
+ onWarning: (w) => console.error(`warning: ${printable(w.message)}`),
120
+ });
121
+ if (f.json)
122
+ console.log(JSON.stringify(r, null, 2));
123
+ else
124
+ printApply(r);
125
+ return r.status === 'FINALIZED' || r.status === 'ALREADY_FINALIZED' ? 0 : r.status === 'REFUSED' ? 2 : 3;
126
+ }
127
+ console.error(`Unknown finalize subcommand: ${sub === undefined ? '(none)' : printable(sub)}\n${USAGE}`);
128
+ return 1;
129
+ }
130
+ catch (err) {
131
+ console.error(err instanceof Error ? err.message : String(err));
132
+ return 1;
133
+ }
134
+ }