deliberate-cli 0.2.0-beta.1.1
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/AGENTS.md +40 -0
- package/LICENSE +174 -0
- package/README.md +89 -0
- package/package.json +51 -0
- package/roles/analyst/frame/instructions.md +88 -0
- package/roles/analyst/frame/output-template.md +52 -0
- package/roles/analyst/launch/instructions.md +63 -0
- package/roles/analyst/launch/output-template.md +50 -0
- package/roles/analyst/one-pager/instructions.md +75 -0
- package/roles/analyst/one-pager/output-template.md +38 -0
- package/roles/analyst/shape/instructions.md +63 -0
- package/roles/analyst/shape/output-template.md +52 -0
- package/roles/briefer/brief/instructions.md +77 -0
- package/roles/briefer/brief/output-template.md +37 -0
- package/roles/config.yaml +130 -0
- package/roles/evaluator/score/instructions.md +84 -0
- package/roles/evaluator/score/output-template.md +11 -0
- package/roles/initiator/init/instructions.md +111 -0
- package/roles/initiator/init/output-template-competitors.md +16 -0
- package/roles/initiator/init/output-template-ecosystem.md +19 -0
- package/roles/initiator/init/output-template-product.md +136 -0
- package/roles/prototyper/prototype/instructions.md +146 -0
- package/roles/prototyper/prototype/output-template.md +10 -0
- package/roles/reporter/readout/instructions.md +54 -0
- package/roles/reporter/readout/output-template.md +37 -0
- package/roles/scout/matchup/instructions.md +74 -0
- package/roles/scout/matchup/output-template.md +115 -0
- package/roles/skills/README.md +19 -0
- package/roles/skills/critique.md +64 -0
- package/roles/skills/head-to-head.md +88 -0
- package/roles/skills/jtbd.md +43 -0
- package/roles/skills/landscape-scan.md +77 -0
- package/roles/skills/metrics.md +58 -0
- package/roles/skills/positioning.md +44 -0
- package/roles/skills/prioritization.md +101 -0
- package/roles/skills/product-readout.md +98 -0
- package/roles/skills/tech-constraints.md +27 -0
- package/roles/skills/ux-principles.md +24 -0
- package/roles/skills/win-conditions.md +68 -0
- package/skill/SKILL.md +231 -0
- package/skill/scripts/deliberate.mjs +44 -0
- package/src/cli/deliberate.mjs +628 -0
- package/src/engine/app-boot.mjs +17 -0
- package/src/engine/briefs.mjs +101 -0
- package/src/engine/cases.mjs +17 -0
- package/src/engine/commands.mjs +75 -0
- package/src/engine/init.mjs +34 -0
- package/src/engine/layout.mjs +37 -0
- package/src/engine/log.mjs +22 -0
- package/src/engine/matchups.mjs +87 -0
- package/src/engine/onepager.mjs +51 -0
- package/src/engine/pipeline.mjs +134 -0
- package/src/engine/projects.mjs +17 -0
- package/src/engine/prompts.mjs +28 -0
- package/src/engine/prototype.mjs +86 -0
- package/src/engine/readout-charts.mjs +217 -0
- package/src/engine/readouts.mjs +132 -0
- package/src/engine/roles.mjs +137 -0
- package/src/engine/scaffold.mjs +54 -0
- package/src/engine/score.mjs +66 -0
- package/src/engine/service.mjs +18 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* briefs.mjs — the Briefer: a project-scoped, time-windowed landscape brief (NOT a case
|
|
3
|
+
* stage). The host runs it in-session (like the Analyst), producing one Markdown artifact
|
|
4
|
+
* that captures the competitive + market changes since the last brief. The engine here is
|
|
5
|
+
* LLM-free: it computes the reporting window, assembles the producer prompt, and persists
|
|
6
|
+
* the produced artifact as `deliberate/briefs/<YYYY-MM-DD>/brief.md`.
|
|
7
|
+
*
|
|
8
|
+
* The window is *since the last brief*, capped at 3 months: `end = now`,
|
|
9
|
+
* `start = max(lastBriefEnd, now − 3 months)` — so a first-ever brief (or a stale one)
|
|
10
|
+
* gets a full 3-month window, and a fresh cadence picks up exactly where the last left off.
|
|
11
|
+
*/
|
|
12
|
+
import { agentConfig } from './roles.mjs';
|
|
13
|
+
import { read, loadBody, loadSkills } from './prompts.mjs';
|
|
14
|
+
import { projectContext, cleanArtifact } from './pipeline.mjs';
|
|
15
|
+
import { unwrapProse } from 'sonorance/plugins/deliberate/markdown.mjs';
|
|
16
|
+
|
|
17
|
+
export const BRIEF_STAGE = 'brief';
|
|
18
|
+
export const BRIEF_WINDOW_MONTHS = 3;
|
|
19
|
+
|
|
20
|
+
// Subtract N calendar months from a timestamp (the "not more than 3 months" floor).
|
|
21
|
+
function monthsBefore(ts, n) { const d = new Date(ts); d.setMonth(d.getMonth() - n); return d.getTime(); }
|
|
22
|
+
|
|
23
|
+
// The reporting window for the NEXT brief of this project. `end` = now; `start` = the
|
|
24
|
+
// previous brief's period_end, but never earlier than 3 months ago (so a first brief or a
|
|
25
|
+
// stale one is capped at a 3-month look-back).
|
|
26
|
+
export function briefWindow(store, pid, at = Date.now()) {
|
|
27
|
+
const floor = monthsBefore(at, BRIEF_WINDOW_MONTHS);
|
|
28
|
+
const last = store.lastBriefEnd(pid);
|
|
29
|
+
const firstEver = last == null;
|
|
30
|
+
const capped = !firstEver && last < floor; // previous brief older than 3 months → floor wins
|
|
31
|
+
const start = firstEver ? floor : Math.max(last, floor);
|
|
32
|
+
return { start, end: at, floor, firstEver, capped, last };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fmtDate = (ts) => new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' });
|
|
36
|
+
export const briefPeriodLabel = (start, end) => `${fmtDate(start)} – ${fmtDate(end)}`;
|
|
37
|
+
|
|
38
|
+
// Strip an optional leading YAML frontmatter block from a stored brief file, leaving the
|
|
39
|
+
// markdown body (the report itself, without the type/id/period metadata).
|
|
40
|
+
const stripFrontmatter = (raw) => { const m = String(raw || '').match(/^---\n[\s\S]*?\n---\n?/); return m ? raw.slice(m[0].length) : String(raw || ''); };
|
|
41
|
+
|
|
42
|
+
// The most recent brief for a project, as a read-only "prior context" block: its period
|
|
43
|
+
// label + full body. This is the grounding for "report only what changed SINCE last time"
|
|
44
|
+
// and "never re-report a change a prior brief already covered". Empty for a first-ever brief.
|
|
45
|
+
export function previousBriefBlock(store, pid) {
|
|
46
|
+
const prev = (store.listBriefs(pid) || [])[0]; // listBriefs is newest-first
|
|
47
|
+
if (!prev) return '';
|
|
48
|
+
const rec = store.readBriefRecord(prev.id);
|
|
49
|
+
const body = stripFrontmatter(rec?.text).trim();
|
|
50
|
+
if (!body) return '';
|
|
51
|
+
return `## Previous brief — read-only prior context (${briefPeriodLabel(prev.period_start, prev.period_end)})\nThis is the brief immediately before the one you are writing. Do NOT re-report anything it already covered; report only what changed AFTER its window. It is the proof a prior brief exists — this is NOT the first brief for this project.\n\n${body}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Describe the window (and why it's bounded the way it is) for the producer prompt. State
|
|
55
|
+
// FIRST-vs-not explicitly and, when a prior brief exists, anchor to its end date — otherwise
|
|
56
|
+
// a tight window (e.g. one that happens to align with project creation) tempts the host to
|
|
57
|
+
// narrate "this is the first brief", which is wrong when an earlier brief exists.
|
|
58
|
+
function windowNote(win) {
|
|
59
|
+
if (win.firstEver) return 'This is the FIRST brief for this project — there is no previous brief. Report changes from the last 3 months (the cap).';
|
|
60
|
+
const prev = fmtDate(win.last);
|
|
61
|
+
if (win.capped) return `This is NOT the first brief for this project (the previous brief ended ${prev}, more than 3 months ago). Report changes from the last 3 months (the cap), not all the way back to it.`;
|
|
62
|
+
return `This is NOT the first brief for this project. The previous brief covered through ${prev}; report only changes since then, and no older.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The exact producer prompt for a brief: grounding + the landscape-scan skill + the Briefer
|
|
66
|
+
// instructions (system) and the reporting window + project competitors + output template
|
|
67
|
+
// (user). The in-harness skill hands this to the host agent, which does the research itself.
|
|
68
|
+
export async function briefPrompt(store, project, { at = Date.now() } = {}) {
|
|
69
|
+
const cfg = agentConfig(BRIEF_STAGE);
|
|
70
|
+
const instruction = await loadBody(cfg.instructions);
|
|
71
|
+
const agents = await read('AGENTS.md');
|
|
72
|
+
const template = await read(cfg.templates.default);
|
|
73
|
+
const skillsBlock = (await loadSkills(cfg.skills)) || '(none)';
|
|
74
|
+
const pctx = projectContext(store, project);
|
|
75
|
+
const competitors = (store.readCompetitors(project.id) || '').trim();
|
|
76
|
+
const compBlock = competitors ? `\n\n## Competitor monitoring sources (competitors.md)\n${competitors}` : '';
|
|
77
|
+
const ecosystem = (store.readEcosystem(project.id) || '').trim();
|
|
78
|
+
const ecoBlock = ecosystem ? `\n\n## Ecosystem monitoring sources (ecosystem.md)\n${ecosystem}` : '';
|
|
79
|
+
const system = `${agents}\n\n${pctx}${compBlock}${ecoBlock}\n\n## Skills\n${skillsBlock}\n\n## Stage instructions\n${instruction}`;
|
|
80
|
+
const win = briefWindow(store, project.id, at);
|
|
81
|
+
const windowBlock = `## Reporting window (STRICT)\n${windowNote(win)}\n- period_start: ${fmtDate(win.start)}\n- period_end: ${fmtDate(win.end)}\nReport ONLY changes dated within this window. Set the brief's "Period:" line to exactly "${briefPeriodLabel(win.start, win.end)}" — just the dates, with no added parenthetical or commentary (never label it a "first brief" or tie it to project creation).`;
|
|
82
|
+
const prevBlock = previousBriefBlock(store, project.id);
|
|
83
|
+
const tpl = template
|
|
84
|
+
? `\n\n----- OUTPUT TEMPLATE -----\n(Fill EVERY section with real, grounded, source-linked content. The italic _..._ lines and parentheticals are guidance for you — replace them; never echo the template's guidance text, and do not add a "grounding"/"notes"/"methodology" section.)\n${template}`
|
|
85
|
+
: '';
|
|
86
|
+
const user = `${windowBlock}${prevBlock ? `\n\n${prevBlock}` : ''}\n\nProduce the brief.${tpl}`;
|
|
87
|
+
return { system, user, template, window: win, model: cfg.model };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Persist a produced brief artifact (LLM-free): clean it, compute the authoritative window,
|
|
91
|
+
// and write it to a new dated brief folder. Shared by the engine and `deliberate brief save`.
|
|
92
|
+
export async function persistBrief(store, project, rawArtifact, { at = Date.now() } = {}) {
|
|
93
|
+
const cfg = agentConfig(BRIEF_STAGE);
|
|
94
|
+
const template = await read(cfg.templates.default);
|
|
95
|
+
const body = unwrapProse(cleanArtifact(rawArtifact, template));
|
|
96
|
+
const win = briefWindow(store, project.id, at);
|
|
97
|
+
const brief = store.createBrief(project.id, {
|
|
98
|
+
period_start: win.start, period_end: win.end, model: cfg.model, body,
|
|
99
|
+
}, at);
|
|
100
|
+
return { brief, window: win };
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cases.mjs — case read helpers. The engine no longer runs the funnel itself: the
|
|
3
|
+
* host harness drives every stage (create → `prompt` → produce → `save`), so there
|
|
4
|
+
* is no headless run, rerun, gate-continue, background scheduler, or auto-resume here.
|
|
5
|
+
* This module just exposes a convenience read (`caseDetail`) over the store.
|
|
6
|
+
*/
|
|
7
|
+
import { STAGES } from 'sonorance/plugins/deliberate/stages.mjs';
|
|
8
|
+
import { STATUS } from 'sonorance/plugins/deliberate/domain.mjs';
|
|
9
|
+
|
|
10
|
+
export const caseDetail = (store, pid, id) => ({
|
|
11
|
+
case: store.getCase(id),
|
|
12
|
+
stages: STAGES.map(name => {
|
|
13
|
+
const st = store.listStages(id).find(s => s.name === name) || { name, status: STATUS.PENDING };
|
|
14
|
+
const summary = st.status === STATUS.DONE ? (st.summary || store.readStage(pid, id, name, 'output_summary.md')) : null;
|
|
15
|
+
return { ...st, summary };
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands.mjs — the SINGLE SOURCE OF TRUTH for Deliberate's command surface.
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces are described here:
|
|
5
|
+
* - SKILL_COMMANDS — the harness-facing `/deliberate` grammar (what a user types).
|
|
6
|
+
* - CLI_COMMANDS — the engine binary (`deliberate …`) the skill shells into.
|
|
7
|
+
*
|
|
8
|
+
* Everything else is derived from these:
|
|
9
|
+
* - `deliberate help` renders CLI_COMMANDS and `deliberate help --skill` renders
|
|
10
|
+
* SKILL_COMMANDS (so either help surface never drifts),
|
|
11
|
+
* - tests fail if the CLI dispatches a command not listed here or omits a registered command.
|
|
12
|
+
*
|
|
13
|
+
* To add, rename, or remove a command, edit this file and update the implementation and shipped
|
|
14
|
+
* skill in the same change. The filesystem grammar lives in layout.mjs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Each entry: [command, one-line description]. `[id]` = optional case id (defaults to the
|
|
18
|
+
// latest case); `<idea>` = free text / URL / file path. Kept sorted alphabetically by command.
|
|
19
|
+
export const SKILL_COMMANDS = [
|
|
20
|
+
['/deliberate address', 'Work through the reader’s in-record comments in the open app — answer or edit, then resolve (optional).'],
|
|
21
|
+
['/deliberate brief', 'Produce a landscape brief — competitive + market changes since the last brief (≤ 3 months).'],
|
|
22
|
+
['/deliberate brief list', 'List the project’s briefs (newest first).'],
|
|
23
|
+
['/deliberate case <idea>', 'Create a case and analyse it (frame → shape → launch); the score and one-pager come with it.'],
|
|
24
|
+
['/deliberate case [id] prototype', 'Build the interactive prototype(s) for a completed case — one per primary surface (asks which case if none is given).'],
|
|
25
|
+
['/deliberate case [id] score', 'Score a completed case with a preferably isolated cross-vendor Evaluator; writes provenance-aware score.md.'],
|
|
26
|
+
['/deliberate case list', 'List all cases and their scores.'],
|
|
27
|
+
['/deliberate feedback', 'Send feedback to the makers — a bug (with repro steps), an idea (framed as the problem you’re solving), or praise; your words are sent verbatim, diagnostics are scrubbed, and never your files.'],
|
|
28
|
+
['/deliberate help', 'Show the current user-facing Deliberate command grammar and a concise description of every command.'],
|
|
29
|
+
['/deliberate init', 'Set up the current repo as a Deliberate project (you read it + write the context).'],
|
|
30
|
+
['/deliberate matchup <competitor>', 'Produce a competitive matchup — a grounded, point-in-time head-to-head against one named rival.'],
|
|
31
|
+
['/deliberate matchup list', 'List the project’s matchups (one per rival; newest first).'],
|
|
32
|
+
['/deliberate readout [period]', 'Produce a product readout for one completed period — the previous calendar week by default, or a natural-language override such as “for June” or “for Q2”.'],
|
|
33
|
+
['/deliberate readout list', 'List the project’s product readouts (newest first).'],
|
|
34
|
+
['/deliberate source add <location> ["<description>"] [--section <section>] | remove <location>', 'Add a categorized context source (with an optional description) or remove one; the host reads it in-harness.'],
|
|
35
|
+
['/deliberate source list', 'List the project’s context sources.'],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Each entry: [invocation, one-line description]. Commands are noun-first, aggregated under
|
|
39
|
+
// their parent (`case`, `brief`, `source`, `comment`). Kept sorted alphabetically by invocation.
|
|
40
|
+
export const CLI_COMMANDS = [
|
|
41
|
+
['brief list', 'List the project’s briefs (newest first; delete one by removing its folder).'],
|
|
42
|
+
['brief prompt', 'Print the Briefer prompt (reporting window + context + template) for the host to fulfill.'],
|
|
43
|
+
['brief save [--file <path>]', 'Persist a produced brief into deliberate/briefs/<date>/brief.md; prints its id.'],
|
|
44
|
+
['case "<idea>"', 'Create a case and make it the active case; prints its id (alias for case new).'],
|
|
45
|
+
['case analysis prompt [id] [--note <text>]', 'Print the next analysis stage’s prompt (frame → shape → launch) for the host to fulfill.'],
|
|
46
|
+
['case analysis save [id] [--file <path>]', 'Persist a produced analysis stage into analysis.md and advance the case.'],
|
|
47
|
+
['case list', 'List all cases and their scores (active marked *). Rename by editing the file; delete by removing the folder.'],
|
|
48
|
+
['case new "<idea>"', 'Create a case and make it the active case; prints its id.'],
|
|
49
|
+
['case one-pager prompt [id]', 'Print the One-pager prompt (an internal reverse PR-FAQ in the customer’s voice) for the host to fulfill.'],
|
|
50
|
+
['case one-pager save [id] [--file <path>]', 'Persist the produced One-pager into the case’s one-pager.md (beside analysis.md).'],
|
|
51
|
+
['case prototype list [id]', 'List the prototypes built for a case (one per primary surface; the default is flat).'],
|
|
52
|
+
['case prototype prompt [id] [--surface <slug>]', 'Print the Prototype prompt (a recomputable companion, built on request) for the host to fulfill; --surface targets one primary surface.'],
|
|
53
|
+
['case prototype save [id] [--surface <slug>] [--file <path>]', 'Persist the produced prototype into the case’s prototype/[<surface>/]index.html companion.'],
|
|
54
|
+
['case score prompt [id]', 'Print the decorrelated Evaluator’s prompt; prefer an isolated cross-vendor sub-agent.'],
|
|
55
|
+
['case score save [id] --model <id> [--independent] [--file <path>]', 'Persist a produced score with its actual evaluator model and independence status into score.md; --independent means an isolated evaluator produced it.'],
|
|
56
|
+
['comment <commentId> resolve [--note "<text>"] [--revised]', 'Resolve an in-record comment back to the app (comment bridge).'],
|
|
57
|
+
['comment list', 'Fetch the open in-record comments from the running app (comment bridge; JSON).'],
|
|
58
|
+
['feedback ["<message>"] [--category <cat>] [--rating <n>] [--file <json>]', 'Send user-authored feedback (bug/idea/praise) — verbatim words + scrubbed diagnostics — mirrored locally, then delivered to the configured telemetry backend (Azure Monitor by default, over OpenTelemetry-shaped signals).'],
|
|
59
|
+
['help [--skill]', 'Print the engine CLI grammar; --skill prints the current user-facing /deliberate grammar.'],
|
|
60
|
+
['init', 'Set up the current folder as a project (deliberate/ + context); name = folder.'],
|
|
61
|
+
['init prompt', 'Print the Initiator prompt (method + the context scaffolds) for the host to fill the project context.'],
|
|
62
|
+
['install [--here | --project <dir>]', 'Install the /deliberate skill (global, or into a repo’s .github/skills).'],
|
|
63
|
+
['matchup list', 'List the project’s matchups (one per rival; newest first; delete one by removing its folder).'],
|
|
64
|
+
['matchup prompt <competitor>', 'Print the Scout prompt (context + the rival + template) for the host to fulfill.'],
|
|
65
|
+
['matchup save <competitor> [--file <path>]', 'Persist a produced matchup into deliberate/matchups/<slug>/matchup.md (refresh-in-place); prints its id.'],
|
|
66
|
+
['readout list', 'List the project’s product readouts (newest first; delete one by removing its folder).'],
|
|
67
|
+
['readout prompt [--period-start <YYYY-MM-DD> --period-end <YYYY-MM-DD>] [--timezone <IANA>]', 'Print the Reporter prompt for one completed reporting period; defaults to the previous calendar week in the supplied timezone (UTC when omitted).'],
|
|
68
|
+
['readout chart --spec <json> --output <svg>', 'Render one validated key-metric time series as a deterministic, accessible SVG trend chart.'],
|
|
69
|
+
['readout save [--file <path> | --bundle <dir>] [--period-start <YYYY-MM-DD> --period-end <YYYY-MM-DD>] [--timezone <IANA>]', 'Persist a product readout only when its Period line matches the selected completed period; for a bundle, also persist referenced charts/ SVG sidecars.'],
|
|
70
|
+
['serve [--open] [--port <n>]', 'Start the local app — the web UI over your vault (--open launches the browser).'],
|
|
71
|
+
['source [list | add <location> ["<description>"] [--section <section>] | remove <location>]', 'List / manage categorized context sources (recorded in .sonorance/sources.md; the host reads each in-harness).'],
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// The distinct top-level verbs the engine CLI must dispatch (first token of each command).
|
|
75
|
+
export const CLI_COMMAND_NAMES = [...new Set(CLI_COMMANDS.map(([c]) => c.split(' ')[0]))];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init.mjs — the Initiator: sets up a project's context. Like the other host-run roles,
|
|
3
|
+
* the engine here is LLM-free — it assembles the producer prompt (the Initiator's method +
|
|
4
|
+
* the three context scaffolds to fill) and the host does the reasoning in-session (reads the
|
|
5
|
+
* repo + attached sources, then edits deliberate/context/{product.md,competitors.md,ecosystem.md}).
|
|
6
|
+
*
|
|
7
|
+
* There is no `init save`: init edits the context files in place (they're written as
|
|
8
|
+
* scaffolds by `deliberate init`), the way a stage's output-template lands in analysis.md.
|
|
9
|
+
*/
|
|
10
|
+
import { agentConfig } from './roles.mjs';
|
|
11
|
+
import { read, loadBody, loadSkills } from './prompts.mjs';
|
|
12
|
+
|
|
13
|
+
export const INIT_STAGE = 'init';
|
|
14
|
+
|
|
15
|
+
// The exact producer prompt for `init`: grounding + skills + the Initiator instructions
|
|
16
|
+
// (system) and the attached sources + the two current context files to fill (user). The
|
|
17
|
+
// in-harness skill hands this to the host, which reads the repo/sources and edits the files.
|
|
18
|
+
export async function initPrompt(store, project) {
|
|
19
|
+
const cfg = agentConfig(INIT_STAGE);
|
|
20
|
+
const instruction = await loadBody(cfg.instructions);
|
|
21
|
+
const agents = await read('AGENTS.md');
|
|
22
|
+
const skillsBlock = (await loadSkills(cfg.skills)) || '(none)';
|
|
23
|
+
const system = `${agents}\n\n## Skills\n${skillsBlock}\n\n## Stage instructions\n${instruction}`;
|
|
24
|
+
const product = (store.readContext(project.id) || '').trim();
|
|
25
|
+
const competitors = (store.readCompetitors(project.id) || '').trim();
|
|
26
|
+
const ecosystem = (store.readEcosystem(project.id) || '').trim();
|
|
27
|
+
const srcRows = store.listSources(project.id);
|
|
28
|
+
const srcs = srcRows.length
|
|
29
|
+
? '\n' + srcRows.map(s => ` - ${s.location}${s.description ? ` — ${s.description}` : ''}`).join('\n')
|
|
30
|
+
: ' none';
|
|
31
|
+
const repo = project.repo ? `\n### Connected repo (read-only): ${project.repo}` : '';
|
|
32
|
+
const user = `Set up the project context for "${project.name}". Read this repo + the attached sources, then EDIT these three files in deliberate/context/ **directly** (they exist as scaffolds — replace the italic guidance with real, grounded content; never fabricate a section you can't ground).\n\n### Attached sources:${srcs}${repo}\n\n----- deliberate/context/product.md -----\n${product}\n\n----- deliberate/context/competitors.md -----\n${competitors}\n\n----- deliberate/context/ecosystem.md -----\n${ecosystem}`;
|
|
33
|
+
return { system, user, model: cfg.model };
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* layout.mjs — the SINGLE SOURCE OF TRUTH for Deliberate's on-disk grammar.
|
|
3
|
+
*
|
|
4
|
+
* Deliberate is files-first (no database); this describes where everything lives. Tests tie
|
|
5
|
+
* these paths to the real resolvers in paths.mjs, so a path change that is not reflected here
|
|
6
|
+
* fails the suite. Paths are relative to a project **vault** (the repo the harness runs in)
|
|
7
|
+
* unless noted; `~/.sonorance/` is the global platform home (shared with Sonorance).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Each entry: [path, description]. `<case>` = `YYYY-MM-DD-slug` (no number; the case's
|
|
11
|
+
// stable hash id lives in the record's frontmatter, and the date sorts chronologically).
|
|
12
|
+
// Kept sorted alphabetically by path.
|
|
13
|
+
export const FS_LAYOUT = [
|
|
14
|
+
['.sonorance/', 'Hidden Sonorance platform state (sibling to deliberate/): shared, cross-skill project configuration. Committed to git.'],
|
|
15
|
+
['.sonorance/config.json', 'Per-vault identity (id, name, repo, created_at) and active case. Committed (don’t hand-edit).'],
|
|
16
|
+
['.sonorance/local/', 'Machine-local runtime state — gitignored. Disposable/regenerable; never shared across machines.'],
|
|
17
|
+
['.sonorance/local/comments.jsonl', 'Generic in-record comments (annotations on any file) — a platform feature, not Deliberate-specific.'],
|
|
18
|
+
['.sonorance/local/state.json', 'Disposable editor state — open tabs, active tab, Explorer collapse/expand. Regenerable; safe to delete.'],
|
|
19
|
+
['.sonorance/plugins.json', 'The vault’s enabled bundled plugin ids — Deliberate registers itself here so `sonorance serve` composes its capabilities additively. Committed.'],
|
|
20
|
+
['.sonorance/sources.md', 'The project’s categorized grounding sources — hand-editable Markdown grouped into product, code, data, customer, go-to-market, and other sections. Committed.'],
|
|
21
|
+
['deliberate/', 'Deliberate’s home in the repo — everything below lives here.'],
|
|
22
|
+
['deliberate/briefs/<YYYY-MM-DD>/brief.md', 'A landscape brief — competitive + market changes since the last brief (≤ 3 months).'],
|
|
23
|
+
['deliberate/cases/<case>/analysis.md', 'The whole case: frontmatter (state, score, per-stage) + the decision record — title, a 1–2 sentence case summary, then one section per stage in funnel order.'],
|
|
24
|
+
['deliberate/cases/<case>/log.jsonl', 'The producer/evaluator run log for the case.'],
|
|
25
|
+
['deliberate/cases/<case>/one-pager.md', 'The internal reverse PR-FAQ — the case in the customer’s voice (narrative + FAQ, PR/FAQ lineage), written beside analysis.md as part of a case.'],
|
|
26
|
+
['deliberate/cases/<case>/prototype/<surface>/index.html', 'An additional interactive prototype for a PRIMARY surface (CLI, API, MCP, …) beyond the default — one per primary surface init marks (`deliberate case prototype --surface <slug>`).'],
|
|
27
|
+
['deliberate/cases/<case>/prototype/index.html', 'The default interactive prototype — a recomputable companion to analysis.md, built on request (`deliberate case prototype`); the single primary surface lives here (extra surfaces nest under prototype/<surface>/).'],
|
|
28
|
+
['deliberate/cases/<case>/score.md', 'The Evaluator’s go/no-go verdict with actual model + independence provenance — a recomputable companion to analysis.md (refresh with `deliberate case score`).'],
|
|
29
|
+
['deliberate/context/*.md', 'Additional context files, read as grounding.'],
|
|
30
|
+
['deliberate/context/competitors.md', 'Official monitoring sources for each competitor (host-written during init).'],
|
|
31
|
+
['deliberate/context/ecosystem.md', 'Official monitoring sources for each ecosystem player — dependencies, complements, channels, movers (host-written during init).'],
|
|
32
|
+
['deliberate/context/product.md', 'The core project context — overview, personas, jobs, competitors, ecosystem, etc. (markdown, human-curated).'],
|
|
33
|
+
['deliberate/matchups/<competitor-slug>/matchup.md', 'A competitive matchup — a grounded, point-in-time head-to-head against one named rival (one canonical doc per rival, refreshed in place).'],
|
|
34
|
+
['deliberate/readouts/<YYYY-MM-DD[-N]>/charts/<metric>.svg', 'Up to three deterministic trend charts for decision-relevant key metrics, generated from normalized completed-period time series and embedded by readout.md.'],
|
|
35
|
+
['deliberate/readouts/<YYYY-MM-DD[-N]>/readout.md', 'A product readout — configured key metrics, optional trend charts, customer evidence, material insights, and warranted actions grounded in one completed reporting period; same-day reruns receive a numeric suffix.'],
|
|
36
|
+
['~/.sonorance/', 'The shared platform home (with Sonorance; override SONORANCE_HOME): the explicit vault registry + current pointer, settings, sonorance.log, and disposable cache. Project content stays in explicitly opened folders.'],
|
|
37
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log.mjs — append-only file log + process crash capture. Everything lands in
|
|
3
|
+
* ~/.sonorance/sonorance.log so failed runs are diagnosable after the fact.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { appHome, logFile } from 'sonorance/plugins/deliberate/paths.mjs';
|
|
7
|
+
|
|
8
|
+
let file;
|
|
9
|
+
function path() { if (!file) { mkdirSync(appHome(), { recursive: true }); file = logFile(); } return file; }
|
|
10
|
+
export function log(level, msg, extra) {
|
|
11
|
+
const line = `${new Date().toISOString()} ${level} ${msg}${extra ? ' ' + (extra.stack || JSON.stringify(extra)) : ''}\n`;
|
|
12
|
+
try { appendFileSync(path(), line); } catch {}
|
|
13
|
+
if (level === 'ERROR') console.error(line.trim()); else console.log(line.trim());
|
|
14
|
+
}
|
|
15
|
+
export const info = (m, e) => log('INFO', m, e);
|
|
16
|
+
export const error = (m, e) => log('ERROR', m, e);
|
|
17
|
+
|
|
18
|
+
// Keep the daemon alive and record why anything blew up.
|
|
19
|
+
export function installCrashHandlers() {
|
|
20
|
+
process.on('uncaughtException', (e) => error('uncaughtException', e));
|
|
21
|
+
process.on('unhandledRejection', (e) => error('unhandledRejection', e instanceof Error ? e : { reason: String(e) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* matchups.mjs — the Scout: a project-scoped, single-competitor head-to-head (NOT a case
|
|
3
|
+
* stage, NOT a brief). The host runs it in-session (like the Briefer), producing one Markdown
|
|
4
|
+
* artifact that reads ONE named rival against us across every dimension — a grounded,
|
|
5
|
+
* point-in-time competitive matchup. The engine here is LLM-free: it assembles the producer
|
|
6
|
+
* prompt (grounding + the rival + the prior matchup for an in-place refresh) and persists the
|
|
7
|
+
* produced artifact as `deliberate/matchups/<competitor-slug>/matchup.md`.
|
|
8
|
+
*
|
|
9
|
+
* Unlike a brief (breadth of change over time, date-keyed, append-only), a matchup is
|
|
10
|
+
* COMPETITOR-KEYED and REFRESHED IN PLACE: one canonical doc per rival, re-run to update
|
|
11
|
+
* (git carries the history; `brief` owns the change-over-time series and signals when a
|
|
12
|
+
* refresh is due). It is always true `as_of` a stated date, stamped in the body AND the
|
|
13
|
+
* frontmatter.
|
|
14
|
+
*/
|
|
15
|
+
import { agentConfig } from './roles.mjs';
|
|
16
|
+
import { read, loadBody, loadSkills } from './prompts.mjs';
|
|
17
|
+
import { projectContext, cleanArtifact } from './pipeline.mjs';
|
|
18
|
+
import { unwrapProse } from 'sonorance/plugins/deliberate/markdown.mjs';
|
|
19
|
+
|
|
20
|
+
export const MATCHUP_STAGE = 'matchup';
|
|
21
|
+
|
|
22
|
+
const fmtDate = (ts) => new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' });
|
|
23
|
+
// The human-readable "as of" label for a matchup (the date the read was true).
|
|
24
|
+
export const matchupAsOfLabel = (ts) => fmtDate(ts);
|
|
25
|
+
|
|
26
|
+
// Strip an optional leading YAML frontmatter block from a stored matchup file, leaving the
|
|
27
|
+
// markdown body (the head-to-head itself, without the type/id/competitor metadata). Also used
|
|
28
|
+
// to scrub any stray frontmatter a producer emits, so the saved file never carries a second
|
|
29
|
+
// metadata block on top of the store-written one.
|
|
30
|
+
const stripFrontmatter = (raw) => { const s = String(raw || '').replace(/^[\s\uFEFF]+/, ''); const m = s.match(/^---\n[\s\S]*?\n---\n?/); return m ? s.slice(m[0].length) : s; };
|
|
31
|
+
|
|
32
|
+
// The existing matchup for this rival, as a read-only "prior read" block: its `as_of` label +
|
|
33
|
+
// full body. This is the grounding for a REFRESH-IN-PLACE — keep what's still true, restamp
|
|
34
|
+
// the date, update only what moved. Empty when there is no prior matchup for the rival.
|
|
35
|
+
export function previousMatchupBlock(store, pid, competitor) {
|
|
36
|
+
const prev = store.matchupForCompetitor(pid, competitor);
|
|
37
|
+
if (!prev) return '';
|
|
38
|
+
const rec = store.readMatchupRecord(prev.id);
|
|
39
|
+
const body = stripFrontmatter(rec?.text).trim();
|
|
40
|
+
if (!body) return '';
|
|
41
|
+
return `## Existing matchup — read-only prior read (as of ${matchupAsOfLabel(prev.as_of || prev.updated_at)})\nThis is the CURRENT canonical matchup for this rival. You are REFRESHING it in place: keep everything still true, correct or update what has changed, restamp the "As of" date, and do NOT start from scratch or spawn a new copy.\n\n${body}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The exact producer prompt for a matchup: grounding + the head-to-head skill (and jtbd /
|
|
45
|
+
// positioning / prioritization / tech-constraints / landscape-scan) + the Scout instructions
|
|
46
|
+
// (system) and the named rival + the as-of date + the prior matchup (for an in-place refresh)
|
|
47
|
+
// + the output template (user). The in-harness skill hands this to the host, which does the
|
|
48
|
+
// competitive research itself.
|
|
49
|
+
export async function matchupPrompt(store, project, competitor, { at = Date.now() } = {}) {
|
|
50
|
+
const rival = String(competitor || '').trim();
|
|
51
|
+
if (!rival) throw new Error('matchup requires a competitor (a name, product, or URL)');
|
|
52
|
+
const cfg = agentConfig(MATCHUP_STAGE);
|
|
53
|
+
const instruction = await loadBody(cfg.instructions);
|
|
54
|
+
const agents = await read('AGENTS.md');
|
|
55
|
+
const template = await read(cfg.templates.default);
|
|
56
|
+
const skillsBlock = (await loadSkills(cfg.skills)) || '(none)';
|
|
57
|
+
const pctx = projectContext(store, project);
|
|
58
|
+
const competitors = (store.readCompetitors(project.id) || '').trim();
|
|
59
|
+
const compBlock = competitors ? `\n\n## Competitor monitoring sources (competitors.md)\n${competitors}` : '';
|
|
60
|
+
const system = `${agents}\n\n${pctx}${compBlock}\n\n## Skills\n${skillsBlock}\n\n## Stage instructions\n${instruction}`;
|
|
61
|
+
const asOf = fmtDate(at);
|
|
62
|
+
const tracked = competitors && new RegExp(`(^|\\n)#+\\s*${rival.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(competitors);
|
|
63
|
+
const rivalBlock = `## The rival (STRICT)\nProduce a single-competitor head-to-head of **${rival}** against this project.\n- competitor: ${rival}\n- as_of: ${asOf}\n${tracked ? '- This rival is already tracked in competitors.md — use its monitoring sources as first-party grounding.' : '- This rival is NOT yet tracked in competitors.md — research it first-party from scratch, then ask the user whether to add it to the project context (default yes): on yes, add it to the Competitors roster in product.md AND its official monitoring sources to competitors.md so `brief` starts tracking it; on no, proceed without touching context.'}\nSet the matchup's "As of" line to exactly "${asOf}" — just the date, no added parenthetical. Every factual claim links to a first-party source, dated on or before ${asOf}; steelman the rival before finding its openings.`;
|
|
64
|
+
const prevBlock = previousMatchupBlock(store, project.id, rival);
|
|
65
|
+
const tpl = template
|
|
66
|
+
? `\n\n----- OUTPUT TEMPLATE -----\n(Fill EVERY section with real, grounded, source-linked content. The italic _..._ lines and parentheticals are guidance for you — replace them; never echo the template's guidance text, and do not add a "grounding"/"notes"/"methodology" section.)\n${template}`
|
|
67
|
+
: '';
|
|
68
|
+
const user = `${rivalBlock}${prevBlock ? `\n\n${prevBlock}` : ''}\n\nProduce the matchup.${tpl}`;
|
|
69
|
+
return { system, user, template, competitor: rival, as_of: at, model: cfg.model };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Persist a produced matchup artifact (LLM-free): clean it, stamp the authoritative as-of
|
|
73
|
+
// date, and upsert it by competitor slug (refresh-in-place). Shared by the engine and
|
|
74
|
+
// `deliberate matchup save`.
|
|
75
|
+
export async function persistMatchup(store, project, competitor, rawArtifact, { at = Date.now() } = {}) {
|
|
76
|
+
const rival = String(competitor || '').trim();
|
|
77
|
+
if (!rival) throw new Error('matchup requires a competitor (a name, product, or URL)');
|
|
78
|
+
const cfg = agentConfig(MATCHUP_STAGE);
|
|
79
|
+
const template = await read(cfg.templates.default);
|
|
80
|
+
// Scrub any frontmatter the producer emits so the store's own frontmatter is the only one
|
|
81
|
+
// (the output template carries none, but a stray block must never double up on the record).
|
|
82
|
+
const body = unwrapProse(stripFrontmatter(cleanArtifact(rawArtifact, template)));
|
|
83
|
+
const matchup = store.createMatchup(project.id, {
|
|
84
|
+
competitor: rival, as_of: at, model: cfg.model, body,
|
|
85
|
+
}, at);
|
|
86
|
+
return { matchup };
|
|
87
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* onepager.mjs — the One-pager: a per-case, INTERNAL reverse-PR-FAQ (customer-voice) companion to the
|
|
3
|
+
* decision record (analysis.md). It is NOT a funnel stage (no score, no gate) — it's an
|
|
4
|
+
* Analyst *synthesis* sub-job the host runs right after the analysis, distilling the
|
|
5
|
+
* finished record (frame + shape + launch) into a concise, customer-voiced narrative +
|
|
6
|
+
* a short FAQ (Amazon Working-Backwards PR/FAQ lineage, minus the jargon).
|
|
7
|
+
*
|
|
8
|
+
* Like the Briefer, the engine here is LLM-free: it assembles the producer prompt
|
|
9
|
+
* (grounding + skills + instructions + the accumulated case record) and deterministically
|
|
10
|
+
* persists what the host produces as `deliberate/cases/<case>/one-pager.md`, beside
|
|
11
|
+
* analysis.md. The decision record links to it (a "## One-pager" section) once it exists.
|
|
12
|
+
*/
|
|
13
|
+
import { agentConfig } from './roles.mjs';
|
|
14
|
+
import { read, loadBody, loadSkills } from './prompts.mjs';
|
|
15
|
+
import { projectContext, caseContext, cleanArtifact } from './pipeline.mjs';
|
|
16
|
+
import { unwrapProse } from 'sonorance/plugins/deliberate/markdown.mjs';
|
|
17
|
+
|
|
18
|
+
// The config key (roles/config.yaml) + role folder for the One-pager sub-job. Not a
|
|
19
|
+
// STAGES member, so it never enters the funnel state machine (like BRIEF_STAGE).
|
|
20
|
+
export const ONEPAGER_STAGE = 'one-pager';
|
|
21
|
+
|
|
22
|
+
// The exact producer prompt for a case's One-pager: grounding + the customer-lens skills
|
|
23
|
+
// (positioning + jtbd) + the One-pager instructions (system) and the accumulated case
|
|
24
|
+
// record — the finished frame/shape/launch it must distil — plus the output template
|
|
25
|
+
// (user). The in-harness skill hands this to the host (the Analyst), who writes it.
|
|
26
|
+
export async function onepagerPrompt(store, project, kase) {
|
|
27
|
+
const cfg = agentConfig(ONEPAGER_STAGE);
|
|
28
|
+
const instruction = await loadBody(cfg.instructions);
|
|
29
|
+
const agents = await read('AGENTS.md');
|
|
30
|
+
const template = await read(cfg.templates.default);
|
|
31
|
+
const skillsBlock = (await loadSkills(cfg.skills)) || '(none)';
|
|
32
|
+
const pctx = projectContext(store, project);
|
|
33
|
+
const system = `${agents}\n\n${pctx}\n\n## Skills\n${skillsBlock}\n\n## Stage instructions\n${instruction}`;
|
|
34
|
+
const ctx = caseContext(store, kase);
|
|
35
|
+
const tpl = template
|
|
36
|
+
? `\n\n----- OUTPUT TEMPLATE -----\n(Fill EVERY section with real, grounded content drawn ONLY from the record below. The italic _..._ lines and parentheticals are guidance for you — replace them; never echo the template's guidance text, and do not add a "grounding"/"notes"/"assumptions" section.)\n${template}`
|
|
37
|
+
: '';
|
|
38
|
+
const user = `Finished case record (distil ONLY this — invent no new capabilities, demand, or evidence):\n${ctx}\nProduce the one-pager.${tpl}`;
|
|
39
|
+
return { system, user, template, model: cfg.model };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Persist a produced One-pager (LLM-free): clean it, unwrap hard-wrapped prose, and write
|
|
43
|
+
// it to `one-pager.md` beside the record (the store also refreshes the record's link).
|
|
44
|
+
// Shared by the engine and `deliberate case one-pager save`.
|
|
45
|
+
export async function persistOnepager(store, project, kase, rawArtifact) {
|
|
46
|
+
const cfg = agentConfig(ONEPAGER_STAGE);
|
|
47
|
+
const template = await read(cfg.templates.default);
|
|
48
|
+
const body = unwrapProse(cleanArtifact(rawArtifact, template));
|
|
49
|
+
store.writeOnepager(kase.id, body);
|
|
50
|
+
return { case: store.getCase(kase.id), file: store.onepagerRef(kase.id) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline.mjs — the stage building blocks the host/skill drives. The engine is
|
|
3
|
+
* LLM-free: it assembles each stage's prompt (grounding + skills + instructions +
|
|
4
|
+
* the accumulated record) via `stagePrompt`, and deterministically persists what the
|
|
5
|
+
* host produces via `persistStage` (clean + heading-normalize + score + advance).
|
|
6
|
+
* The host agent does all the reasoning; there is no engine-run funnel.
|
|
7
|
+
*/
|
|
8
|
+
import { nextStage } from 'sonorance/plugins/deliberate/stages.mjs';
|
|
9
|
+
import { STATE, STATUS } from 'sonorance/plugins/deliberate/domain.mjs';
|
|
10
|
+
import { agentConfig } from './roles.mjs';
|
|
11
|
+
import { read, loadBody, loadSkills } from './prompts.mjs';
|
|
12
|
+
import { unwrapProse } from 'sonorance/plugins/deliberate/markdown.mjs';
|
|
13
|
+
|
|
14
|
+
export const scoreOf = (t) => { const m = t.match(/(?:total weighted score|score)[:*\s]+(10(?:\.0)?|[0-9](?:\.\d)?)/i); return m ? +m[1] : null; };
|
|
15
|
+
|
|
16
|
+
// Deterministically scrub an artifact before it's saved as an end-user deliverable:
|
|
17
|
+
// 1. drop any line the agent copied verbatim from the output template's italic
|
|
18
|
+
// "_…_" guidance (matched by emphasis/whitespace-normalized text, so a
|
|
19
|
+
// reformatted copy is still caught) — the "never echo the template" rule, enforced;
|
|
20
|
+
// 2. drop internal/meta commentary about the run, session, or the agent's own
|
|
21
|
+
// execution (e.g. "already produced in my previous response", "the task was to…").
|
|
22
|
+
// Never touches lines inside fenced code blocks (e.g. the prototype's HTML). Prose is
|
|
23
|
+
// unwrapped separately, per stage — NOT here — so the prototype's bare HTML/JS (which is
|
|
24
|
+
// newline-sensitive) is never reflowed.
|
|
25
|
+
const NORM = (s) => s.replace(/[*_`>]/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
26
|
+
const IS_ITALIC_LINE = (t) => /^(?:>\s*|[-*]\s+|\d+[.)]\s+)?_[^_].*_$/.test(t);
|
|
27
|
+
const META_RE = /\b(?:my |the )?previous (?:response|answer|session|run|turn)\b|\balready (?:produced|generated|created|completed|provided)\b|\bthe task was to\b|\bas (?:an? )?(?:ai|assistant|language model)\b|\bi (?:have )?(?:already )?(?:produced|generated|created|completed|provided|wrote|written)\b|\bin (?:my|this) (?:previous |earlier )?(?:response|answer|session|run|turn)\b|\bthis (?:completes|completed) the task\b|\b(?:no|any) (?:further|additional) (?:action|output) (?:is )?(?:needed|required)\b/i;
|
|
28
|
+
export function cleanArtifact(art, template) {
|
|
29
|
+
const guide = new Set();
|
|
30
|
+
for (const l of (template || '').split('\n')) { const t = l.trim(); if (IS_ITALIC_LINE(t)) { const n = NORM(t); if (n) guide.add(n); } }
|
|
31
|
+
const out = []; let fence = false;
|
|
32
|
+
for (const line of (art || '').split('\n')) {
|
|
33
|
+
if (/^\s*```/.test(line)) { fence = !fence; out.push(line); continue; }
|
|
34
|
+
if (!fence) {
|
|
35
|
+
const t = line.trim();
|
|
36
|
+
if (t && guide.has(NORM(t))) continue; // echoed template guidance
|
|
37
|
+
if (t && META_RE.test(t)) continue; // internal/run/execution meta
|
|
38
|
+
}
|
|
39
|
+
out.push(line);
|
|
40
|
+
}
|
|
41
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Assemble the case's accumulated context: the case (its 1–2 sentence summary once
|
|
45
|
+
// written, else the raw prompt while it's still transient) + every prior stage's FULL
|
|
46
|
+
// artifact (not a lossy summary), passed to each role so it builds on — and never
|
|
47
|
+
// contradicts or loses — the work before it. With only a few coherent stages feeding
|
|
48
|
+
// one document, full context beats summary-chaining ("context is king").
|
|
49
|
+
export function caseContext(store, kase) {
|
|
50
|
+
const prior = store.listStages(kase.id)
|
|
51
|
+
.filter(s => s.status === STATUS.DONE)
|
|
52
|
+
.map(s => `## ${s.name}\n${store.readStage(kase.project_id, kase.id, s.name, 'output_full.md') || ''}`)
|
|
53
|
+
.join('\n\n');
|
|
54
|
+
return `# ${kase.title}\n\n## Case\n${kase.summary || kase.description || ''}\n\n${prior}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Per-project context block: the host-written markdown context (product.md) +
|
|
58
|
+
// attached sources + optional read-only repo, prepended to every stage's system
|
|
59
|
+
// prompt so Copilot is grounded in THIS project, not just the repo-level skills.
|
|
60
|
+
export function projectContext(store, project) {
|
|
61
|
+
const ctx = (store.readContext(project.id) || '').trim();
|
|
62
|
+
const srcRows = store.listSources(project.id);
|
|
63
|
+
// Each source is passed WITH its (user-editable) description so the stage agents
|
|
64
|
+
// know what each attached source is and why it's relevant — not just a bare URL.
|
|
65
|
+
const srcs = srcRows.length
|
|
66
|
+
? '\n' + srcRows.map(s => ` - [${s.sectionTitle || 'Other'}] ${s.location}${s.description ? ` — ${s.description}` : ''}`).join('\n')
|
|
67
|
+
: ' none';
|
|
68
|
+
const body = ctx || `_Project context not written yet (run \`/deliberate init\`). Product: ${project.name}._`;
|
|
69
|
+
return `## Project context (read-only)\n\n${body}\n\n### Attached sources:${srcs}` +
|
|
70
|
+
(project.repo ? `\n### Connected repo (read-only): ${project.repo}` : '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Reusable stage building blocks (the host/skill drives these; the engine only
|
|
74
|
+
// builds the prompt and deterministically persists what the host produces) ----
|
|
75
|
+
|
|
76
|
+
// The exact producer prompt for a stage: grounding + skills + instructions (system)
|
|
77
|
+
// and the accumulated per-case context + output template (user). The in-harness
|
|
78
|
+
// skill hands this to the host agent / a Task sub-agent; the engine feeds it to
|
|
79
|
+
// `infer`. Returns `tpl` too (the template block reused for the revise turn).
|
|
80
|
+
export async function stagePrompt(store, project, kase, stage, note = null) {
|
|
81
|
+
const cfg = agentConfig(stage);
|
|
82
|
+
const instruction = await loadBody(cfg.instructions);
|
|
83
|
+
const agents = await read('AGENTS.md');
|
|
84
|
+
const template = await read(cfg.templates.default);
|
|
85
|
+
const skillsBlock = (await loadSkills(cfg.skills)) || '(none)';
|
|
86
|
+
const pctx = projectContext(store, project);
|
|
87
|
+
const system = `${agents}\n\n${pctx}\n\n## Skills\n${skillsBlock}\n\n## Stage instructions\n${instruction}`;
|
|
88
|
+
const ctx = caseContext(store, kase);
|
|
89
|
+
const tpl = template ? `\n\n----- OUTPUT TEMPLATE -----\n(Fill EVERY section with real, grounded content. The italic _..._ lines and parentheticals are guidance for you — replace them with your actual analysis. NEVER echo the template's guidance, placeholder, or instruction text in your output, and do not add a "grounding", "assumptions", or "notes" section.)\n${template}` : '';
|
|
90
|
+
const user = `Context so far:\n${ctx}\nProduce ${stage}.${tpl}${note ? `\nIterate note: ${note}` : ''}`;
|
|
91
|
+
return { system, user, template, tpl, model: cfg.model };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const STAGE_LABEL = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
95
|
+
|
|
96
|
+
// Embed a stage artifact under its `## <Stage>` section in the combined record: drop
|
|
97
|
+
// the artifact's own leading `# <Stage>` title (it duplicates the section header) and
|
|
98
|
+
// demote every heading one level (## → ###, etc.) so the record keeps ONE clean
|
|
99
|
+
// hierarchy. Headings inside fenced code blocks are left untouched.
|
|
100
|
+
function embedArtifact(md, stage) {
|
|
101
|
+
const label = STAGE_LABEL(stage);
|
|
102
|
+
const out = []; let fence = false, seenHeading = false;
|
|
103
|
+
for (const line of (md || '').split('\n')) {
|
|
104
|
+
if (/^\s*```/.test(line)) { fence = !fence; out.push(line); continue; }
|
|
105
|
+
const h = !fence && line.match(/^(#{1,6})\s+(.*\S)\s*$/);
|
|
106
|
+
if (h) {
|
|
107
|
+
if (!seenHeading && h[1] === '#' && h[2].trim().toLowerCase() === label.toLowerCase()) { seenHeading = true; continue; }
|
|
108
|
+
seenHeading = true;
|
|
109
|
+
out.push('#'.repeat(Math.min(6, h[1].length + 1)) + ' ' + h[2]);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out.push(line);
|
|
113
|
+
}
|
|
114
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Persist a completed stage artifact (LLM-free): clean it, normalize its headings for
|
|
118
|
+
// the combined record, write the body section, record the score, and advance the case.
|
|
119
|
+
// Shared by the engine run and `deliberate case analysis save`. (Only the three funnel
|
|
120
|
+
// stages flow through here; the score/one-pager/prototype companions persist separately.)
|
|
121
|
+
export async function persistStage(store, project, caseId, stage, rawArtifact) {
|
|
122
|
+
const cfg = agentConfig(stage);
|
|
123
|
+
const template = await read(cfg.templates.default);
|
|
124
|
+
const art = cleanArtifact(rawArtifact, template);
|
|
125
|
+
const sc = scoreOf(art);
|
|
126
|
+
// Embed each stage's heading-normalized artifact, with hard-wrapped prose unwrapped so
|
|
127
|
+
// the saved Markdown isn't artificially chopped into lines.
|
|
128
|
+
const files = { 'output_full.md': unwrapProse(embedArtifact(art, stage)) };
|
|
129
|
+
store.writeStage(project.id, caseId, stage, files);
|
|
130
|
+
store.setStage(caseId, stage, { status: STATUS.DONE, score: sc, ended_at: Date.now(), model: cfg.model });
|
|
131
|
+
const next = nextStage(stage);
|
|
132
|
+
store.setCase(caseId, { state: next || STATE.DONE });
|
|
133
|
+
return { case: store.getCase(caseId), stage, score: sc, next };
|
|
134
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projects.mjs — project resolution + the shared "current project" pointer.
|
|
3
|
+
*
|
|
4
|
+
* A project is a **vault** (a plain folder that is its own source of truth).
|
|
5
|
+
* Deliberate only opens explicit folders; it never fabricates hidden projects or
|
|
6
|
+
* resolves commands against a globally selected vault outside the current folder.
|
|
7
|
+
*/
|
|
8
|
+
import { scaffoldContext } from './scaffold.mjs';
|
|
9
|
+
|
|
10
|
+
// Register an explicit folder as a project vault. Used by `deliberate init`.
|
|
11
|
+
export function openProjectVault(store, dir, opts = {}) {
|
|
12
|
+
return scaffoldContext(store.openVaultFolder(dir, opts));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setCurrentProject(store, id) {
|
|
16
|
+
store.setCurrent(id);
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts.mjs — shared prompt-assembly helpers used by both the pipeline stages
|
|
3
|
+
* (src/engine/pipeline.mjs) and the auxiliary contextualize agent
|
|
4
|
+
* for the agents: read a repo-relative file, strip an optional YAML
|
|
5
|
+
* frontmatter block, and render a skills block from a list of skill paths.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
|
+
|
|
13
|
+
// Read a repo-relative file; missing/unreadable files resolve to '' (callers degrade gracefully).
|
|
14
|
+
export const read = async (rel) => { try { return await readFile(join(ROOT, rel), 'utf8'); } catch { return ''; } };
|
|
15
|
+
|
|
16
|
+
// Strip an optional YAML frontmatter block; skills live in roles/config.yaml,
|
|
17
|
+
// so only the instruction body is needed downstream.
|
|
18
|
+
export async function loadBody(rel) {
|
|
19
|
+
const raw = await read(rel);
|
|
20
|
+
const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
21
|
+
return fm ? raw.slice(fm[0].length) : raw;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Skills are configured as repo-relative paths; the heading is the file's base name.
|
|
25
|
+
const skillName = (p) => p.replace(/^.*\//, '').replace(/\.md$/, '');
|
|
26
|
+
export const loadSkills = async (paths) => paths?.length
|
|
27
|
+
? (await Promise.all(paths.map(async p => `### ${skillName(p)}\n${await read(p)}`))).join('\n\n')
|
|
28
|
+
: '';
|