planr 1.10.0-alpha.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +11 -0
- package/CHANGELOG.md +20 -0
- package/README.md +27 -245
- package/docs/board.md +44 -0
- package/docs/cli.md +100 -0
- package/docs/concepts.md +133 -0
- package/docs/contract.md +150 -0
- package/docs/plugins/bb.md +49 -0
- package/docs/plugins/claude-code.md +84 -0
- package/docs/plugins/codex.md +77 -0
- package/docs/quickstart.md +307 -0
- package/docs/setup.md +103 -0
- package/package.json +21 -68
- package/plugin.json +12 -0
- package/skills/planr/SKILL.md +64 -0
- package/skills/planr/assets/board/fonts/LICENSE-mona-sans.txt +93 -0
- package/skills/planr/assets/board/fonts/LICENSE-monaspace.txt +93 -0
- package/skills/planr/assets/board/fonts/MonaSans-Variable.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-400.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-500.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-600.woff2 +0 -0
- package/skills/planr/assets/board/template.html +788 -0
- package/skills/planr/assets/templates/goal.md +13 -0
- package/skills/planr/assets/templates/hook.sh +4 -0
- package/skills/planr/assets/templates/in-repo.config.json +6 -0
- package/skills/planr/assets/templates/planr.config.json +6 -0
- package/skills/planr/assets/templates/tasks-README.md +14 -0
- package/skills/planr/references/format.md +88 -0
- package/skills/planr/references/work-a-goal.md +62 -0
- package/skills/planr/scripts/lib/board.mjs +39 -0
- package/skills/planr/scripts/lib/graph.mjs +345 -0
- package/skills/planr/scripts/lib/runs.mjs +127 -0
- package/skills/planr/scripts/lib/version.mjs +11 -0
- package/skills/planr/scripts/planr.mjs +386 -0
- package/docs/ARCHITECTURE.md +0 -136
- package/docs/RELEASE.md +0 -275
- package/docs/SWITCHLOOM_COMPATIBILITY.md +0 -14
- package/docs/contracts/BROWSER_HARNESS_ADAPTER_V1.md +0 -75
- package/docs/contracts/EVAL_CONTRACT_V1.md +0 -2072
- package/docs/contracts/EVIDENCE_ADAPTER_PROTOCOL_V1.md +0 -98
- package/docs/contracts/EVIDENCE_CONTRACT_V1.md +0 -382
- package/docs/contracts/GROK_INTEGRATION_V1.md +0 -139
- package/docs/contracts/LINUX_RELEASE_PORTABILITY.json +0 -9
- package/docs/contracts/PI_INTEGRATION_V1.md +0 -164
- package/docs/contracts/V1_1_DIFFERENTIATION_CONTRACT.md +0 -177
- package/docs/contracts/fixtures/evidence/v1/examples/coverage-verdict.json +0 -66
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-policy.json +0 -108
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-receipt.json +0 -93
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-waiver.json +0 -31
- package/docs/contracts/fixtures/evidence/v1/examples/proof-obligation.json +0 -55
- package/docs/contracts/fixtures/evidence/v1/examples/proof-preset.json +0 -19
- package/docs/contracts/fixtures/evidence/v1/examples/untrusted-evidence-proposal.json +0 -20
- package/docs/contracts/fixtures/evidence/v1/examples/verification-capability-instance.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/examples/verification-capability-manifest.json +0 -52
- package/docs/contracts/fixtures/evidence/v1/negative/aborted-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/adapter-registration-missing-execution-contract.json +0 -88
- package/docs/contracts/fixtures/evidence/v1/negative/available-empty-probe-capability.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/blocked-coverage-with-stale-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/empty-trust-bindings-receipt.json +0 -72
- package/docs/contracts/fixtures/evidence/v1/negative/failed-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/forged-trusted-receipt.json +0 -35
- package/docs/contracts/fixtures/evidence/v1/negative/inconclusive-clean-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/inconclusive-coverage-with-blocked-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/minimal-policy-missing-owned-inputs.json +0 -5
- package/docs/contracts/fixtures/evidence/v1/negative/mismatched-probe-execution-id-capability.json +0 -42
- package/docs/contracts/fixtures/evidence/v1/negative/passed-empty-attempt.json +0 -22
- package/docs/contracts/fixtures/evidence/v1/negative/policy-default-preset-unregistered.json +0 -102
- package/docs/contracts/fixtures/evidence/v1/negative/policy-weakening-without-waiver.json +0 -102
- package/docs/contracts/fixtures/evidence/v1/negative/process-probe-missing-output-limits.json +0 -49
- package/docs/contracts/fixtures/evidence/v1/negative/satisfied-empty-coverage-verdict.json +0 -14
- package/docs/contracts/fixtures/evidence/v1/negative/skipped-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/stale-coverage-with-inconclusive-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/timed-out-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/unavailable-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/unbound-waiver.json +0 -21
- package/docs/contracts/fixtures/evidence/v1/negative/unsatisfied-coverage-without-gap.json +0 -63
- package/docs/contracts/fixtures/evidence/v1/negative/waived-coverage-forbidden-gap-reason.json +0 -28
- package/docs/contracts/fixtures/evidence/v1/negative/waived-coverage-missing-waiver-id.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/schemas/evidence-contract-v1.schema.json +0 -2685
- package/docs/contracts/schemas/com.planr.web.dom_state.v1.schema.json +0 -30
- package/docs/documentation/CONTRACT.md +0 -164
- package/docs/documentation/COVERAGE.md +0 -98
- package/docs/documentation/INFORMATION_ARCHITECTURE.md +0 -151
- package/docs/fixtures/mcp-contract.json +0 -153
- package/npm/bin/native-launcher.js +0 -67
- package/npm/bin/planr-browser-harness-adapter.js +0 -8
- package/npm/bin/planr.js +0 -8
- package/npm/native/darwin-arm64/planr +0 -0
- package/npm/native/darwin-arm64/planr-browser-harness-adapter +0 -0
- package/npm/native/darwin-arm64/planr-host-capability-validator +0 -0
- package/npm/native/darwin-x86_64/planr +0 -0
- package/npm/native/darwin-x86_64/planr-browser-harness-adapter +0 -0
- package/npm/native/darwin-x86_64/planr-host-capability-validator +0 -0
- package/npm/native/linux-arm64/planr +0 -0
- package/npm/native/linux-arm64/planr-browser-harness-adapter +0 -0
- package/npm/native/linux-arm64/planr-host-capability-validator +0 -0
- package/npm/native/linux-x86_64/planr +0 -0
- package/npm/native/linux-x86_64/planr-browser-harness-adapter +0 -0
- package/npm/native/linux-x86_64/planr-host-capability-validator +0 -0
- package/plugins/planr/.claude-plugin/plugin.json +0 -11
- package/plugins/planr/.codex-plugin/plugin.json +0 -25
- package/plugins/planr/agents/pi/planr-reviewer.md +0 -20
- package/plugins/planr/agents/pi/planr-worker.md +0 -35
- package/plugins/planr/agents/planr-reviewer.md +0 -21
- package/plugins/planr/agents/planr-worker.md +0 -40
- package/plugins/planr/skills/planr/SKILL.md +0 -60
- package/plugins/planr/skills/planr-goal/SKILL.md +0 -52
- package/plugins/planr/skills/planr-loop/SKILL.md +0 -126
- package/plugins/planr/skills/planr-loop/agents/planr-reviewer.md +0 -21
- package/plugins/planr/skills/planr-loop/agents/planr-worker.md +0 -36
- package/plugins/planr/skills/planr-loop/references/host-dispatch.md +0 -52
- package/plugins/planr/skills/planr-loop/references/recovery-and-verification.md +0 -26
- package/plugins/planr/skills/planr-plan/SKILL.md +0 -86
- package/plugins/planr/skills/planr-review/SKILL.md +0 -55
- package/plugins/planr/skills/planr-status/SKILL.md +0 -52
- package/plugins/planr/skills/planr-summary/SKILL.md +0 -30
- package/plugins/planr/skills/planr-task-graph/SKILL.md +0 -67
- package/plugins/planr/skills/planr-verify-web/SKILL.md +0 -62
- package/plugins/planr/skills/planr-work/SKILL.md +0 -93
- package/scripts/host-capability-experiment.mjs +0 -2481
- package/scripts/host-capability-runtime/v1/schemas/host-capability-expected-manifest.schema.json +0 -377
- package/scripts/host-capability-runtime/v1/schemas/host-capability-observed-raw.schema.json +0 -439
- package/scripts/host-capability-runtime/v1/schemas/host-capability-provenance.schema.json +0 -178
- package/scripts/planr-host-capability-validator +0 -57
- /package/{LICENSE.md → LICENSE} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { posix } from 'node:path';
|
|
3
|
+
import { compare, taskFile, taskStatus } from './graph.mjs';
|
|
4
|
+
|
|
5
|
+
export function git(root, args, input) {
|
|
6
|
+
try {
|
|
7
|
+
const value = execFileSync('git', args, {
|
|
8
|
+
cwd: root,
|
|
9
|
+
encoding: input === undefined ? 'utf8' : undefined,
|
|
10
|
+
input,
|
|
11
|
+
maxBuffer: 64 << 20,
|
|
12
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
13
|
+
});
|
|
14
|
+
return typeof value === 'string' ? value.trimEnd() : value;
|
|
15
|
+
} catch {
|
|
16
|
+
return input === undefined ? '' : Buffer.alloc(0);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runs(root, index, c, now = new Date().toISOString()) {
|
|
21
|
+
const mode = git(root, ['rev-parse', '--is-shallow-repository']);
|
|
22
|
+
const warnings = mode === 'false' ? [] : [
|
|
23
|
+
mode ? 'history: shallow clone, runs incomplete'
|
|
24
|
+
: 'history: not a git repository, runs unavailable',
|
|
25
|
+
];
|
|
26
|
+
const prefix = git(root, ['rev-parse', '--show-prefix']) +
|
|
27
|
+
posix.normalize(c.tasksDir).replace(/\/$/, '') + '/';
|
|
28
|
+
const log = mode ? git(root, [
|
|
29
|
+
'log', '--first-parent', '--reverse', '--format=%x00PLANR%x09%H%x09%cI%x09%s',
|
|
30
|
+
'--raw', '-z', '--no-abbrev', '--no-renames', '--root', '--diff-merges=first-parent', '--', c.tasksDir,
|
|
31
|
+
]) : '';
|
|
32
|
+
const records = [];
|
|
33
|
+
const parts = log.split('\0');
|
|
34
|
+
for (let i = 0; i < parts.length; i++) {
|
|
35
|
+
const part = parts[i].replace(/^\n/, '');
|
|
36
|
+
if (part.startsWith('PLANR\t')) {
|
|
37
|
+
const [, full, date, ...subject] = part.split('\t');
|
|
38
|
+
records.push({ commit: full.slice(0, 7), date, subject: subject.join('\t'), files: [] });
|
|
39
|
+
} else if (part.startsWith(':')) {
|
|
40
|
+
const [, , , blob, status] = part.split(' ');
|
|
41
|
+
const file = parts[++i];
|
|
42
|
+
if (file?.startsWith(prefix) && taskFile(file.slice(prefix.length))) {
|
|
43
|
+
records.at(-1).files.push({ file, blob, removed: status === 'D' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const hashes = [...new Set(records.flatMap(r =>
|
|
48
|
+
r.files.filter(f => !f.removed).map(f => f.blob)
|
|
49
|
+
))];
|
|
50
|
+
const batch = hashes.length
|
|
51
|
+
? git(root, ['cat-file', '--batch'], hashes.join('\n') + '\n')
|
|
52
|
+
: Buffer.alloc(0);
|
|
53
|
+
const blobs = new Map();
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
while (cursor < batch.length) {
|
|
56
|
+
const end = batch.indexOf('\n', cursor);
|
|
57
|
+
if (end < 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
const [hash, type, bytes] = batch.subarray(cursor, end).toString().split(' ');
|
|
61
|
+
cursor = end + 1;
|
|
62
|
+
if (type === 'blob') {
|
|
63
|
+
const size = Number(bytes);
|
|
64
|
+
blobs.set(hash, batch.subarray(cursor, cursor + size).toString('utf8'));
|
|
65
|
+
cursor += size + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const result = [];
|
|
69
|
+
const history = new Map(index.tasks.map(t => [t.id, []]));
|
|
70
|
+
let previous = new Map();
|
|
71
|
+
const changesBetween = current => [...new Set([...previous.keys(), ...current.keys()])]
|
|
72
|
+
.sort(compare)
|
|
73
|
+
.filter(id => current.get(id) !== previous.get(id))
|
|
74
|
+
.map(id => ({ id, from: previous.get(id) ?? null, to: current.get(id) ?? 'removed' }));
|
|
75
|
+
const append = (s, current) => {
|
|
76
|
+
const changes = changesBetween(current);
|
|
77
|
+
if (changes.length) {
|
|
78
|
+
const initial = !result.length;
|
|
79
|
+
result.push({
|
|
80
|
+
commit: s.commit,
|
|
81
|
+
date: s.date,
|
|
82
|
+
subject: s.subject,
|
|
83
|
+
counts: [...current.values()].reduce((out, status) => {
|
|
84
|
+
out[status] = (out[status] ?? 0) + 1;
|
|
85
|
+
return out;
|
|
86
|
+
}, {}),
|
|
87
|
+
total: current.size,
|
|
88
|
+
initial,
|
|
89
|
+
changes: initial ? [] : changes,
|
|
90
|
+
});
|
|
91
|
+
for (const change of (initial ? [] : changes)) {
|
|
92
|
+
history.get(change.id)?.push({ run: result.length - 1, from: change.from, to: change.to });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
previous = current;
|
|
96
|
+
};
|
|
97
|
+
for (const record of records) {
|
|
98
|
+
const current = new Map(previous);
|
|
99
|
+
for (const f of record.files) {
|
|
100
|
+
const id = f.file.slice(prefix.length, -3);
|
|
101
|
+
if (f.removed) {
|
|
102
|
+
current.delete(id);
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
current.set(id, taskStatus(blobs.get(f.blob) ?? ''));
|
|
106
|
+
} catch {
|
|
107
|
+
warnings.push(`history: ${record.commit} ${f.file} unreadable, skipped`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
append(record, current);
|
|
112
|
+
}
|
|
113
|
+
if (mode) {
|
|
114
|
+
append({ commit: null, date: now, subject: 'Uncommitted changes' },
|
|
115
|
+
new Map(index.tasks.map(t => [t.id, t.status])));
|
|
116
|
+
}
|
|
117
|
+
const tasks = index.tasks.map(t => {
|
|
118
|
+
const h = history.get(t.id);
|
|
119
|
+
return {
|
|
120
|
+
...t,
|
|
121
|
+
goal: index.goals.filter(g => g.state !== 'done' && g.scope.includes(t.id)).map(g => g.name),
|
|
122
|
+
history: h,
|
|
123
|
+
lastRun: h.at(-1)?.run ?? null,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { runs: result, tasks, warnings };
|
|
127
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export function version() {
|
|
4
|
+
const skill = readFileSync(new URL('../../SKILL.md', import.meta.url), 'utf8');
|
|
5
|
+
const frontmatter = skill.split(/^---\r?$/m)[1] ?? '';
|
|
6
|
+
const match = frontmatter.match(/^metadata:\r?\n version: "([^"]+)"\r?$/m);
|
|
7
|
+
if (!match) {
|
|
8
|
+
throw Error('SKILL.md is missing metadata.version');
|
|
9
|
+
}
|
|
10
|
+
return match[1];
|
|
11
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, chmodSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve, join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import {
|
|
7
|
+
config, findRoot, buildIndex, json, compare, statuses, states, rewrite, nextWork,
|
|
8
|
+
} from './lib/graph.mjs';
|
|
9
|
+
import { boardData, render, writeBoard } from './lib/board.mjs';
|
|
10
|
+
import { git, runs } from './lib/runs.mjs';
|
|
11
|
+
import { version } from './lib/version.mjs';
|
|
12
|
+
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
const options = {};
|
|
15
|
+
const positional = [];
|
|
16
|
+
const flags = ['json', 'strict', 'ready', 'serve', 'in-repo'];
|
|
17
|
+
const allowed = {
|
|
18
|
+
check: ['strict'],
|
|
19
|
+
index: ['strict'],
|
|
20
|
+
list: ['status', 'track', 'owner', 'goal', 'ready'],
|
|
21
|
+
next: ['goal', 'stream'],
|
|
22
|
+
set: ['owner'],
|
|
23
|
+
goal: ['thread'],
|
|
24
|
+
board: ['out', 'serve', 'port'],
|
|
25
|
+
init: ['in-repo'],
|
|
26
|
+
};
|
|
27
|
+
const message = text => console.error(`planr: ${String(text).replace(/[\r\n]+/g, ' ')}`);
|
|
28
|
+
const usage = text => {
|
|
29
|
+
const e = Error(text);
|
|
30
|
+
e.code = 2;
|
|
31
|
+
throw e;
|
|
32
|
+
};
|
|
33
|
+
const output = (value, lines = []) => options.json
|
|
34
|
+
? console.log(json(value).trimEnd())
|
|
35
|
+
: lines.forEach(line => console.log(`planr: ${String(line).replace(/[\r\n]+/g, ' ')}`));
|
|
36
|
+
|
|
37
|
+
function checked(root, c) {
|
|
38
|
+
const result = buildIndex(root, c);
|
|
39
|
+
if (result.errors.length) {
|
|
40
|
+
const e = Error(result.errors.join('; '));
|
|
41
|
+
e.code = 1;
|
|
42
|
+
e.errors = result.errors;
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
return result.index;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function check(root, c, target) {
|
|
49
|
+
const result = buildIndex(root, c);
|
|
50
|
+
const warnings = [...result.index.warnings];
|
|
51
|
+
if (!result.errors.length) {
|
|
52
|
+
warnings.push(...runs(root, result.index, c).warnings);
|
|
53
|
+
}
|
|
54
|
+
const errors = options.strict ? [...result.errors, ...warnings] : result.errors;
|
|
55
|
+
process.exitCode = errors.length ? 1 : 0;
|
|
56
|
+
if (target) {
|
|
57
|
+
const scope = target.id ? [target.id]
|
|
58
|
+
: result.index.goals.find(g => g.name === target.name)?.scope ?? target.scope;
|
|
59
|
+
const files = [target.file, ...result.index.tasks.filter(t => scope.includes(t.id)).map(t => t.file)];
|
|
60
|
+
const relevant = warnings.filter(w =>
|
|
61
|
+
scope.some(id => w.startsWith(`${id}:`) || w.includes(`: dependency ${id} is dropped;`)) ||
|
|
62
|
+
files.some(file => w.startsWith(`${file}:`) ||
|
|
63
|
+
w.endsWith(` ${file} unreadable, skipped`) || w.endsWith(`/${file} unreadable, skipped`)
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
index: result.index,
|
|
68
|
+
ok: !errors.length,
|
|
69
|
+
errors,
|
|
70
|
+
warnings: relevant,
|
|
71
|
+
otherWarnings: warnings.length - relevant.length,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
output(
|
|
75
|
+
{ ok: !errors.length, errors, warnings },
|
|
76
|
+
[...warnings, ...result.errors, errors.length ? 'check failed' : 'check passed']
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mutationOutput(value, line, validation) {
|
|
81
|
+
const { index, ...diagnostics } = validation;
|
|
82
|
+
output({ ...value, ...diagnostics }, [
|
|
83
|
+
line,
|
|
84
|
+
...diagnostics.errors,
|
|
85
|
+
...diagnostics.warnings,
|
|
86
|
+
...(diagnostics.otherWarnings ? [`${diagnostics.otherWarnings} other warnings (run check)`] : []),
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function init(root) {
|
|
91
|
+
const planning = options['in-repo'] ? join(root, '.planr') : root;
|
|
92
|
+
const source = fileURLToPath(new URL('../assets/templates/', import.meta.url));
|
|
93
|
+
const create = (file, content, mode) => {
|
|
94
|
+
if (existsSync(file)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
98
|
+
writeFileSync(file, content);
|
|
99
|
+
if (mode) {
|
|
100
|
+
chmodSync(file, mode);
|
|
101
|
+
}
|
|
102
|
+
message(`created ${file}`);
|
|
103
|
+
};
|
|
104
|
+
for (const [from, to] of [
|
|
105
|
+
['tasks-README.md', 'tasks/README.md'],
|
|
106
|
+
['goal.md', 'plans/example-goal.md'],
|
|
107
|
+
]) {
|
|
108
|
+
create(join(planning, to), readFileSync(join(source, from)));
|
|
109
|
+
}
|
|
110
|
+
const configTemplate = options['in-repo'] ? 'in-repo.config.json' : 'planr.config.json';
|
|
111
|
+
create(join(root, 'planr.config.json'), readFileSync(join(source, configTemplate)));
|
|
112
|
+
const ignore = join(root, '.gitignore');
|
|
113
|
+
const previous = existsSync(ignore) ? readFileSync(ignore, 'utf8') : '';
|
|
114
|
+
const entry = '.planr/board.html';
|
|
115
|
+
const lines = (previous.match(/[^\n]*\n|[^\n]+$/g) ?? []).filter(s =>
|
|
116
|
+
!['.planr/', '/.planr/', '.planr'].includes(s.trim())
|
|
117
|
+
);
|
|
118
|
+
let content = lines.join('');
|
|
119
|
+
if (!lines.some(s => [entry, '/' + entry, entry.replace(/\/$/, '')].includes(s.trim()))) {
|
|
120
|
+
content += (content && !content.endsWith('\n') ? '\n' : '') + entry + '\n';
|
|
121
|
+
}
|
|
122
|
+
if (content !== previous) {
|
|
123
|
+
writeFileSync(ignore, content);
|
|
124
|
+
}
|
|
125
|
+
const hooks = git(root, ['rev-parse', '--git-path', 'hooks']);
|
|
126
|
+
if (hooks) {
|
|
127
|
+
for (const hook of ['post-commit', 'post-merge']) {
|
|
128
|
+
const file = resolve(root, hooks, hook);
|
|
129
|
+
const old = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
130
|
+
const generated = old.startsWith('#!/bin/sh\nentry="') && old.split('\n').length === 5 &&
|
|
131
|
+
old.endsWith('node "$entry" board >/dev/null 2>&1 || true\n');
|
|
132
|
+
if (old === '#!/bin/sh\nplanr board\n' || generated) {
|
|
133
|
+
rmSync(file);
|
|
134
|
+
message(`removed ${file}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
create(
|
|
138
|
+
resolve(root, hooks, 'pre-commit'),
|
|
139
|
+
readFileSync(join(source, 'hook.sh'), 'utf8').replace('/*ENTRY*/', () =>
|
|
140
|
+
fileURLToPath(import.meta.url).replace(/["\\$`]/g, ch => '\\' + ch)
|
|
141
|
+
),
|
|
142
|
+
0o755
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
message('initialized');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function serve(root, c, port) {
|
|
149
|
+
let html;
|
|
150
|
+
let version = 0;
|
|
151
|
+
let signature;
|
|
152
|
+
const scan = () => {
|
|
153
|
+
const data = [git(root, ['rev-parse', 'HEAD']), git(root, ['rev-parse', '--git-path', 'HEAD'])];
|
|
154
|
+
for (const dir of [c.tasksDir, c.goalsDir]) {
|
|
155
|
+
if (existsSync(join(root, dir))) {
|
|
156
|
+
for (const f of readdirSync(join(root, dir), { recursive: true }).sort()) {
|
|
157
|
+
try {
|
|
158
|
+
data.push(f, readFileSync(join(root, dir, f), 'utf8'));
|
|
159
|
+
} catch {
|
|
160
|
+
/* directories */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const head = data[1];
|
|
166
|
+
if (head && existsSync(resolve(root, head))) {
|
|
167
|
+
data.push(readFileSync(resolve(root, head), 'utf8'));
|
|
168
|
+
}
|
|
169
|
+
const next = JSON.stringify(data);
|
|
170
|
+
if (next === signature) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
signature = next;
|
|
174
|
+
const page = render(boardData(root, checked(root, c), c));
|
|
175
|
+
version++;
|
|
176
|
+
html = page.replace('</body>', () => `<script>
|
|
177
|
+
setInterval(async()=>{
|
|
178
|
+
try {
|
|
179
|
+
if ((await (await fetch('/version',{cache:'no-store'})).text()) !== '${version}') {
|
|
180
|
+
location.reload();
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
},700);
|
|
184
|
+
</script></body>`);
|
|
185
|
+
};
|
|
186
|
+
scan();
|
|
187
|
+
const timer = setInterval(() => {
|
|
188
|
+
try {
|
|
189
|
+
scan();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
message(e.message);
|
|
192
|
+
}
|
|
193
|
+
}, 500);
|
|
194
|
+
const server = createServer((req, res) => {
|
|
195
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
196
|
+
res.setHeader('Content-Type', req.url === '/version' ? 'text/plain' : 'text/html; charset=utf-8');
|
|
197
|
+
if (!['/', '/version'].includes(req.url)) {
|
|
198
|
+
res.writeHead(404);
|
|
199
|
+
res.end();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
res.end(req.url === '/version' ? String(version) : html);
|
|
203
|
+
});
|
|
204
|
+
server.on('error', e => {
|
|
205
|
+
clearInterval(timer);
|
|
206
|
+
message(e.message);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
});
|
|
209
|
+
server.listen(port, '127.0.0.1', () => message(`serving http://127.0.0.1:${server.address().port}`));
|
|
210
|
+
const stop = () => {
|
|
211
|
+
clearInterval(timer);
|
|
212
|
+
server.close();
|
|
213
|
+
};
|
|
214
|
+
process.on('SIGINT', stop);
|
|
215
|
+
process.on('SIGTERM', stop);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (argv.length === 1 && argv[0] === '--version') {
|
|
220
|
+
console.log(`planr ${version()}`);
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
if (!argv.length || (argv.length === 1 && argv[0] === '--help')) {
|
|
224
|
+
console.log(`Usage: planr <command> [options]
|
|
225
|
+
|
|
226
|
+
Commands:
|
|
227
|
+
check [--strict] Validate tasks and goals (index is a deprecated alias)
|
|
228
|
+
list [--ready] List tasks; filter by --status, --track, --owner or --goal
|
|
229
|
+
next --goal <name> Show ready and waiting tasks (--stream <S> also works)
|
|
230
|
+
set <id> <status> Change task status [--owner <stream>]
|
|
231
|
+
goal <name> [<state>] Show or change a goal [--thread <id>]
|
|
232
|
+
board [--out <file>] Render the board [--serve] [--port <n>]
|
|
233
|
+
init [--in-repo] Scaffold a planning repository
|
|
234
|
+
|
|
235
|
+
Options: --root <dir>, --json, --help, --version`);
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
for (let i = 0; i < argv.length; i++) {
|
|
239
|
+
if (!argv[i].startsWith('--')) {
|
|
240
|
+
positional.push(argv[i]);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const key = argv[i].slice(2);
|
|
244
|
+
if (key in options) {
|
|
245
|
+
usage(`duplicate --${key}`);
|
|
246
|
+
}
|
|
247
|
+
if (flags.includes(key)) {
|
|
248
|
+
options[key] = true;
|
|
249
|
+
} else {
|
|
250
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) {
|
|
251
|
+
usage(`--${key} requires a value`);
|
|
252
|
+
}
|
|
253
|
+
options[key] = argv[++i];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const [command, id, value, ...extra] = positional;
|
|
257
|
+
if (!allowed[command]) {
|
|
258
|
+
usage('usage: planr check | list | next | set | goal | board | init');
|
|
259
|
+
}
|
|
260
|
+
for (const key of Object.keys(options)) {
|
|
261
|
+
if (!['root', 'json', ...allowed[command]].includes(key)) {
|
|
262
|
+
usage(`unknown option --${key}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const limit = ['set', 'goal'].includes(command) ? 3 : 1;
|
|
266
|
+
if (positional.length > limit || extra.length) {
|
|
267
|
+
usage('too many arguments');
|
|
268
|
+
}
|
|
269
|
+
if (command === 'set' && (!id || !statuses.includes(value))) {
|
|
270
|
+
usage('set requires a task ID and a known status');
|
|
271
|
+
}
|
|
272
|
+
if (command === 'goal' && (!id || (value && !states.includes(value)))) {
|
|
273
|
+
usage('goal requires a name and an optional known state');
|
|
274
|
+
}
|
|
275
|
+
if (command === 'next' && !!options.goal === !!options.stream) {
|
|
276
|
+
usage('next requires exactly one of --goal or --stream');
|
|
277
|
+
}
|
|
278
|
+
const root = options.root
|
|
279
|
+
? resolve(options.root)
|
|
280
|
+
: command === 'init'
|
|
281
|
+
? process.cwd()
|
|
282
|
+
: findRoot(process.cwd());
|
|
283
|
+
if (command === 'init') {
|
|
284
|
+
init(root);
|
|
285
|
+
} else {
|
|
286
|
+
const c = config(root);
|
|
287
|
+
if (['check', 'index'].includes(command)) {
|
|
288
|
+
if (command === 'index') {
|
|
289
|
+
message('index is deprecated, use check');
|
|
290
|
+
}
|
|
291
|
+
check(root, c);
|
|
292
|
+
} else {
|
|
293
|
+
const data = checked(root, c);
|
|
294
|
+
const goal = name => {
|
|
295
|
+
const g = data.goals.find(g => g.name === name);
|
|
296
|
+
if (!g) {
|
|
297
|
+
usage(`unknown goal ${name}`);
|
|
298
|
+
}
|
|
299
|
+
return g;
|
|
300
|
+
};
|
|
301
|
+
const lines = tasks => tasks.map(t =>
|
|
302
|
+
`${t.id} ${t.status} ${t.track} ${t.title} (unlocks ${t.unlocks})`
|
|
303
|
+
);
|
|
304
|
+
if (command === 'list') {
|
|
305
|
+
const order = ['blocked', 'decision', 'in-progress', 'review', 'open', 'done', 'dropped'];
|
|
306
|
+
const scope = options.goal ? goal(options.goal).scope : null;
|
|
307
|
+
const tasks = data.tasks
|
|
308
|
+
.filter(t =>
|
|
309
|
+
(!options.status || options.status.split(',').includes(t.status)) &&
|
|
310
|
+
(!options.track || t.track === options.track) &&
|
|
311
|
+
(!options.owner || t.owner === options.owner) &&
|
|
312
|
+
(!scope || scope.includes(t.id)) &&
|
|
313
|
+
(!options.ready || t.ready)
|
|
314
|
+
)
|
|
315
|
+
.sort((a, b) =>
|
|
316
|
+
order.indexOf(a.status) - order.indexOf(b.status) ||
|
|
317
|
+
b.waitingOn.length - a.waitingOn.length ||
|
|
318
|
+
compare(a.id, b.id)
|
|
319
|
+
);
|
|
320
|
+
output(tasks, lines(tasks));
|
|
321
|
+
} else if (command === 'next') {
|
|
322
|
+
const goals = options.goal
|
|
323
|
+
? [goal(options.goal)]
|
|
324
|
+
: data.goals.filter(g =>
|
|
325
|
+
g.stream === options.stream && ['approved', 'running'].includes(g.state)
|
|
326
|
+
);
|
|
327
|
+
const stream = options.stream || goals[0].stream;
|
|
328
|
+
const { ready, waiting } = nextWork(data, goals, stream);
|
|
329
|
+
output(
|
|
330
|
+
{ [options.goal ? 'goal' : 'stream']: options.goal || options.stream, ready, waiting },
|
|
331
|
+
['ready', ...lines(ready), 'waiting', ...lines(waiting)]
|
|
332
|
+
);
|
|
333
|
+
} else if (command === 'set') {
|
|
334
|
+
const task = data.tasks.find(t => t.id === id);
|
|
335
|
+
if (!task) {
|
|
336
|
+
usage(`unknown task ${id}`);
|
|
337
|
+
}
|
|
338
|
+
rewrite(join(root, task.file), {
|
|
339
|
+
status: value,
|
|
340
|
+
...(options.owner ? { owner: options.owner } : {}),
|
|
341
|
+
});
|
|
342
|
+
const validation = check(root, c, task);
|
|
343
|
+
mutationOutput(
|
|
344
|
+
{ id, from: task.status, to: value }, `${id}: ${task.status} -> ${value}`, validation
|
|
345
|
+
);
|
|
346
|
+
} else if (command === 'goal') {
|
|
347
|
+
const g = goal(id);
|
|
348
|
+
let validation;
|
|
349
|
+
if (value || options.thread) {
|
|
350
|
+
rewrite(join(root, g.file), {
|
|
351
|
+
...(value ? { state: value } : {}),
|
|
352
|
+
...(options.thread ? { thread: options.thread } : {}),
|
|
353
|
+
});
|
|
354
|
+
validation = check(root, c, g);
|
|
355
|
+
}
|
|
356
|
+
const updated = (validation?.index ?? data).goals.find(g => g.name === id) ?? g;
|
|
357
|
+
const line = `${updated.name} ${updated.state} ${updated.done}/${updated.total}` +
|
|
358
|
+
` thread=${updated.thread ?? '-'}`;
|
|
359
|
+
if (validation) {
|
|
360
|
+
mutationOutput(updated, line, validation);
|
|
361
|
+
} else {
|
|
362
|
+
output(updated, [line]);
|
|
363
|
+
}
|
|
364
|
+
} else if (command === 'board') {
|
|
365
|
+
if (options.port && (!/^\d+$/.test(options.port) || +options.port > 65535)) {
|
|
366
|
+
usage('port must be 0..65535');
|
|
367
|
+
}
|
|
368
|
+
message(`wrote ${writeBoard(root, data, c, options.out).file}`);
|
|
369
|
+
if (options.serve) {
|
|
370
|
+
serve(root, c, +(options.port ?? 4173));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch (e) {
|
|
376
|
+
if (options.json) {
|
|
377
|
+
console.log(json({
|
|
378
|
+
ok: false,
|
|
379
|
+
errors: e.errors ?? [e.message],
|
|
380
|
+
warnings: [],
|
|
381
|
+
}).trimEnd());
|
|
382
|
+
} else {
|
|
383
|
+
message(e.message);
|
|
384
|
+
}
|
|
385
|
+
process.exitCode = typeof e.code === 'number' ? e.code : 1;
|
|
386
|
+
}
|
package/docs/ARCHITECTURE.md
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# Planr Architecture
|
|
2
|
-
|
|
3
|
-
Planr V1 is a single Rust binary with explicit module ownership. The crate stays small enough that a Cargo workspace would add more process overhead than value today, and there is only one deployable: the `planr` CLI. The source tree is split by ownership boundary inside that crate instead of using a premature workspace. Shared mutations that must behave identically across CLI, MCP, and HTTP live in one place (`src/app/application.rs`) so the three surfaces cannot drift.
|
|
4
|
-
|
|
5
|
-
## Repository Layout
|
|
6
|
-
|
|
7
|
-
- `src/`: the Rust CLI (module ownership below).
|
|
8
|
-
- `tests/e2e.rs`: real CLI, MCP, HTTP, import, review-gate, run-log, and concurrent-pick tests.
|
|
9
|
-
- `plugins/planr/`: the installable plugin payload — all ten skills, independent Claude/Cursor worker and reviewer role assets, and the per-host plugin manifests. Planr does not ship model-pinned Codex roles.
|
|
10
|
-
- `.agents/plugins/marketplace.json`, `.claude-plugin/marketplace.json`: marketplace manifests pointing Codex and Claude Code at `plugins/planr`.
|
|
11
|
-
- `docs/`: user and contributor guides; `docs/contracts/` holds the frozen product contracts that gate CI.
|
|
12
|
-
- `.planr/plans/product/planr/`: the production specification package for Planr V1, stored as a normal Planr product plan package. It is the only committed `.planr` path; all other runtime state stays local.
|
|
13
|
-
- `examples/real-world-flow.md`: executable real-world operator flow.
|
|
14
|
-
- `scripts/`: installer and release packaging scripts.
|
|
15
|
-
- `npm/`: the npm wrapper package.
|
|
16
|
-
|
|
17
|
-
## Module Ownership
|
|
18
|
-
|
|
19
|
-
- `src/main.rs`: process composition root. Owns top-level module wiring, process startup, database opening, error printing, and dispatch into `App`.
|
|
20
|
-
- `src/cli.rs`: CLI contract boundary. Owns `clap` command definitions, option parsing types, value enums, and command DTOs used by app dispatch.
|
|
21
|
-
- `src/app/mod.rs`: application composition boundary. Owns the `App` runtime state, top-level dispatch, shared app-local row helpers, and app submodule wiring.
|
|
22
|
-
- `src/app/commands.rs`: general CLI use-case orchestration. Owns project, plan, map, item, link, pick, approval, log, close, review, context, search, doctor, and shared install command handlers.
|
|
23
|
-
- `src/app/grok.rs`: Grok-specific install orchestration. Owns safe project TOML reconciliation, repository workflow writes, and the explicit no-hooks result.
|
|
24
|
-
- `src/app/prompts.rs`: CLI, MCP, and HTTP prompt output. Host routing prompt composition remains in `src/app/agents.rs`.
|
|
25
|
-
- `src/app/flow.rs`: compound workflow boundary. Owns evidence log writing (with heartbeat folding), canonical FeatureRun settlement input normalization, the close transition core, the pick work packet, and the `done` command shared by CLI, HTTP, and MCP surfaces.
|
|
26
|
-
- `src/app/git_review.rs`: Git and PR review evidence boundary. Owns worktree detection, scoped changed-file provenance, PR URL context, and dirty-worktree safety projections.
|
|
27
|
-
- `src/app/mcp.rs`: MCP stdio boundary. Owns MCP protocol request routing, tool calls, resource reads, and prompt responses.
|
|
28
|
-
- `src/app/packages.rs`: package import/export boundary. Owns reusable JSON templates, preview-before-import, durable ReviewGate package projections, and local-first encrypted bundle metadata.
|
|
29
|
-
- `src/app/http.rs`: localhost HTTP/SSE boundary. Owns HTTP request parsing, routes, SSE stream output, and HTTP response mapping.
|
|
30
|
-
- `src/app/repository.rs`: application data access helpers. Owns Planr query/update helpers over projects, plans, graph items, links, runs, logs, artifacts, events, approvals, search, and map projections.
|
|
31
|
-
- `src/app/lease.rs`: worker lease ownership. Owns the single pick query (`PickFilter`: exclude, work type, plan scope), worker ownership checks, runtime heartbeat/progress/pause state, and stale-pick detection.
|
|
32
|
-
- `src/app/review.rs`: ReviewGate application logic. Owns review annotations, feedback ingestion, scoped evidence, durable attempt completion, finding resolution, and gate lookup.
|
|
33
|
-
- `src/app/recovery.rs`: recovery automation logic. Owns item retry policy configuration, task conditions, stale/timed-out sweeps, retry scheduling, and recovery result projections.
|
|
34
|
-
- `src/app/execution_run.rs`: canonical FeatureRun and ReviewGate application boundary. Owns run phases, outcome batching and settlement, typed escalation, role leases, durable findings/re-review, and final product review projections.
|
|
35
|
-
- `src/app/execution_state.rs`: canonical `planr.execution_state.v2` read boundary. It projects one run-scoped FeatureRun, active batch, role owner, persisted budget amounts/provenance/digest/deadline, ReviewGate, attempts, findings, stable reason code, and next action for CLI, MCP, HTTP, pick, trace, status, recovery, package, and audit consumers.
|
|
36
|
-
- `src/app/surfaces.rs`: non-CLI runtime surfaces. Owns trace, scrub, artifact, event, debug, export, and import command handlers.
|
|
37
|
-
- `src/app/inspection.rs`: local inspection helpers. Owns debug bundles, context/link snapshots, pick context, secret scans, export value assembly, run recording, search results, and Planr-directory import parsing.
|
|
38
|
-
- `src/app/audit.rs`: goal contract audit boundary. Owns the clause-by-clause `plan audit` verdict (items settled, required independent material reviews, exactly one current independent final product review, approvals clear, canonical Evidence coverage) and its human rendering. Claim-only verification logs remain isolated to frozen pre-Evidence compatibility.
|
|
39
|
-
- `src/app/application.rs`: shared surface-mutation boundary. Owns the approval request/approve/deny, context, log, artifact, and close mutations reused verbatim by CLI, MCP, and HTTP handlers so the three surfaces cannot drift.
|
|
40
|
-
- `src/app/repository/`: focused data-access submodules (`item.rs`, `plan.rs`, `project.rs`, `link.rs`, `context.rs`, `evidence.rs`, `execution_run.rs`, `search.rs`) split out of `src/app/repository.rs` by entity ownership. Execution-state reads select ReviewGates strictly by run id; project/plan-wide history is never projected as the current run.
|
|
41
|
-
- `src/model.rs`: JSON-facing data transfer types and typed vocabulary. Owns serializable Planr DTOs plus the `ItemStatus`, `WorkType`, `LinkKind`, and `ApprovalStatus` enums with their parsing and display behavior, used by CLI JSON, MCP, HTTP, storage rows, and tests.
|
|
42
|
-
- `src/storage/mod.rs`: SQLite connection boundary. Owns default database path, connection setup, pragma configuration, and storage submodule exports.
|
|
43
|
-
- `src/storage/schema.rs`: SQLite schema boundary. Owns DDL, additive schema upgrade helpers, and schema version recording.
|
|
44
|
-
- `src/storage/execution_run_schema.rs`: the sole persisted compatibility boundary for FeatureRun rollout. It upgrades already-stored review/fix history into durable terminal history, but no application, packet, agent asset, or documentation path produces the superseded live shape.
|
|
45
|
-
- `src/storage/rows.rs`: SQLite row mapping boundary. Owns row-to-DTO and row-to-JSON mapping functions.
|
|
46
|
-
- `src/planpack.rs`: Markdown package generation and parsing. Owns project context templates, product/build plan templates, plan metadata parsing, hashes, search body extraction, and task extraction.
|
|
47
|
-
- `src/agents.rs`: agent profile registry core. Owns `.planr/agents.toml` parsing, registry validation warnings, and the pure advisory `resolve_route` precedence logic (override > work_type > plan > default); no storage or host concerns.
|
|
48
|
-
- `src/usage_policy.rs`: provider-neutral Usage Policy v1 core. Owns strict `.planr/policy.toml` parsing, policy and task-contract vocabulary, materiality classification, budget/concurrency validation, and the pure five-way transition resolver; it contains no provider ids, host dispatch, or execution-permission behavior.
|
|
49
|
-
- `src/execution_policy.rs`: execution admission core. Owns per-role filesystem, network, tool/MCP, structured command, environment, hook, secret, and approval grants; permission-diff previews; bounded task-contract admission; fail-closed command grammar; and isolated write-scope concurrency. It never selects models or mutates graph state. `src/app/policy.rs` binds that pure decision to the current SQLite lease owner and pick token, and only treats an admission from that exact lease generation as authoritative.
|
|
50
|
-
- `src/route_audit.rs`: provider-neutral run-observation contract. Owns strict requested/resolved/effective route stages, model/effort/fork enforcement confidence, transition provenance, policy/binding versions, and per-dimension metering confidence. It rejects requested-only values in the effective stage rather than inferring host execution.
|
|
51
|
-
- `src/app/agents.rs`: routing application boundary. Owns the `agents` and `item route` command handlers, the shared `*_value` JSON shapes reused by MCP, per-item route facts assembly, and registry-aware role content selection for installs.
|
|
52
|
-
- `src/app/agents_init.rs`: registry bootstrap boundary. Owns `planr agents init` — the static cost-tiering scaffold and, per the agent-pool plan, the flag-spec builder and interactive wizard.
|
|
53
|
-
- `src/integrations.rs`: shared agent-client descriptor boundary. Owns MCP install metadata, tool schemas, resources, and text response wrapping.
|
|
54
|
-
- `src/integrations/grok.rs`: document-preserving Grok project MCP configuration and conflict policy.
|
|
55
|
-
- `src/rolefiles.rs`: static host workflow roles and Cursor skill payloads. It does not select or pin models; externally generated routing artifacts stay outside Planr ownership.
|
|
56
|
-
- `src/util.rs`: small CLI-boundary utilities. Owns ids, timestamps, path helpers, output formatting, and safe file writes.
|
|
57
|
-
|
|
58
|
-
## FeatureRun Execution Boundary
|
|
59
|
-
|
|
60
|
-
Every live worker decision is a typed work packet:
|
|
61
|
-
|
|
62
|
-
- `kind: "outcome"` is ordinary maker work; `mode: "finding_repair"` returns named findings to the same responsible maker and ReviewGate without creating a fix item.
|
|
63
|
-
- `kind: "review_gate"` is independently leased checker work. Attempts and findings remain children of that gate, never graph items.
|
|
64
|
-
- `kind: "verification"` is a coordinator-owned verifier lease over a frozen canonical source digest. Product source is read-only; trusted Evidence can commit only through the source-checked Evidence transaction. The coordinator calls `evidence verify` without another model.
|
|
65
|
-
- Releasing verification is an application-owned transition: the item lease, FeatureRun verifier role, and verification budget reconcile atomically back to `source_frozen`. `pick release --repair <reference>` additionally invalidates the frozen candidate and routes a fresh repair batch to the recorded maker; the failed verifier lease is never resumed.
|
|
66
|
-
- An accepted material checkpoint is source-bound after any product or admission repair. Repair settlement preserves the prior attempt, reopens the same gate with the active freeze revision/digest and repair-obligation lineage, and leaves verification unleaseable until a fresh independent acceptance of that exact binding.
|
|
67
|
-
- `kind: "hold"` is an admission or capability stop. A driver must report its classification and next action; it cannot reinterpret the hold as permission to replace the maker, weaken verification, or open an ad hoc review.
|
|
68
|
-
|
|
69
|
-
All four packets embed the same run-scoped `planr.execution_state.v2` projection. Host skills, generated roles, installed copies, and Stop hooks render that state but own no lifecycle or budget policy. Ordinary maker settlement uses plain `planr done`; only an allowed protected-risk interrupt uses structured escalation. Review findings are logged and resolved on the existing gate. Evidence runs once after stable source freeze and selectively reruns only invalidated obligations after a product repair.
|
|
70
|
-
|
|
71
|
-
The one exception is persisted database input in `src/storage/execution_run_schema.rs`. Existing user databases may contain historical review/fix graph rows, so the schema upgrade consumes them once into terminal history. That read boundary does not justify aliases, fallback commands, dual DTOs, or live legacy producers elsewhere.
|
|
72
|
-
|
|
73
|
-
## Planr 2.0 Immutable Budget Boundary
|
|
74
|
-
|
|
75
|
-
The Planr 2.0 budget authority is one immutable `planr.feature_run_budget_contract.v2` created atomically with each FeatureRun. Bounded contracts contain a persisted UTC run-start anchor, complete wall-seconds/tool-call/token totals, exact maker/verification/review/repair/release allocations, per-dimension metering requirements, and a canonical digest. Unbounded mode is an explicit contract with no numeric limits or reserves. Every admitted bounded task carries positive maxima for all three dimensions and an overflow-checked absolute UTC deadline.
|
|
76
|
-
|
|
77
|
-
Ownership is singular and dependencies are one-way:
|
|
78
|
-
|
|
79
|
-
- `src/usage_policy.rs` owns the provider-neutral contract types, validation, provenance, phase protection, snapshots, and pure checked arithmetic.
|
|
80
|
-
- `src/execution_policy.rs` owns pure admission over typed concurrency plus persisted budget snapshots.
|
|
81
|
-
- `src/app/execution_run.rs` resolves authored policy once and binds the immutable contract to run creation.
|
|
82
|
-
- `src/app/feature_run_evidence.rs` owns transactional reservation, append-only observation, reconciliation, phase release, and hold sequencing.
|
|
83
|
-
- `src/app/execution_state.rs` owns the sole `planr.execution_state.v2` projection reused by CLI, MCP, HTTP, work packets, trace, status, generated roles, and skills.
|
|
84
|
-
- Storage owns insert-only persistence and integrity mechanics; host adapters only enforce supplied maxima/deadlines and report observations with provenance.
|
|
85
|
-
|
|
86
|
-
Runtime decisions never reload `.planr/policy.toml`, fabricate allowances, infer trusted usage from projections, synthesize missing active-run state, or expose a compatibility budget DTO. Missing, invalid, corrupt, or unenforceable budget state is a typed hold before dispatch.
|
|
87
|
-
|
|
88
|
-
An incompatible active FeatureRun is retired only through the plan-scoped application lifecycle in `src/app/execution_run.rs`. Pure eligibility and policy-cancel state changes live in `src/execution_run.rs`; `src/app/repository/execution_run.rs` ends the batch and leases, updates the run with optimistic concurrency, and records provenance in one transaction. CLI, MCP, and HTTP only transport the closed `incompatible-budget` reason and reuse the canonical result. Restart never writes a budget contract or successor run; a later ordinary pick remains the sole atomic run-plus-contract creation path.
|
|
89
|
-
|
|
90
|
-
A compatible budget-held FeatureRun resumes only through `planr run resolve-budget-hold --plan <id>`, owned by `src/app/feature_run_evidence.rs`. The application transaction revalidates the immutable contract, persisted snapshot, active reservation deadlines, exact held phase, canonical role owner, and lease generation before restoring that phase. Incompatible contracts require restart; capability holds, corrupt state, expired deadlines, missing reservations, unrepaired ceilings, and owner mismatches remain held. MCP and HTTP transport the same typed application result without policy or arithmetic.
|
|
91
|
-
|
|
92
|
-
## Eval Contract V1/V1.1 Ownership
|
|
93
|
-
|
|
94
|
-
The frozen product contract lives in `docs/contracts/EVAL_CONTRACT_V1.md` until implementation promotes it into code. Its V1.1 efficiency-evidence amendment is additive and keeps the same owner split:
|
|
95
|
-
|
|
96
|
-
- repository EvalSuite manifests and fixtures are authored inputs;
|
|
97
|
-
- the suite loader owns validation, safe path resolution, normalization, canonical ordering, and digests;
|
|
98
|
-
- SQLite owns immutable suite snapshots, runs, case results, samples, attempt lineage, metering-basis provenance, comparisons, invalidations, rescoring provenance, and eval evidence references;
|
|
99
|
-
- the comparison engine owns verdicts, reason codes, matched effective-treatment compatibility, and derived failure-inclusive efficiency metrics;
|
|
100
|
-
- existing map/log/review/approval state remains authoritative for work closure, so eval evidence may support a review but must never close or reopen items by itself.
|
|
101
|
-
|
|
102
|
-
## Boundary Rules
|
|
103
|
-
|
|
104
|
-
- Command parsing belongs in `src/cli.rs`; process startup belongs in `src/main.rs`; command execution belongs under `src/app/`.
|
|
105
|
-
- `src/main.rs` must stay small enough to be only a composition root. It must not own product use cases.
|
|
106
|
-
- `src/app/mod.rs` must stay small enough to wire runtime state and dispatch. It must not absorb app submodule behavior.
|
|
107
|
-
- SQLite schema belongs in `src/storage/schema.rs`; row mapping belongs in `src/storage/rows.rs`; app data access helpers belong in `src/app/repository.rs` and its submodules.
|
|
108
|
-
- Mutations shared by more than one surface (CLI, MCP, HTTP) belong in `src/app/application.rs`; surface handlers must call the shared helper instead of repeating SQL.
|
|
109
|
-
- Markdown templates belong in `planpack.rs`; command handlers should request generated file sets instead of embedding large template bodies.
|
|
110
|
-
- Agent install metadata and MCP schema descriptors belong in `src/integrations.rs`; client-specific strings should not drift across command handlers and docs.
|
|
111
|
-
- General graph DTO and vocabulary-enum changes belong in `src/model.rs`; FeatureRun/ExecutionBatch/ReviewGate status projection changes belong in `src/app/execution_state.rs`. JSON response shapes reuse those owners before adding ad hoc maps.
|
|
112
|
-
- Item status, work type, link kind, and approval status values are typed enums; new states must be added to the enum, not smuggled in as strings.
|
|
113
|
-
- Utility code must stay narrow. If a helper starts owning product behavior, move it to the owning module instead of growing `util.rs`.
|
|
114
|
-
- Do not add catch-all `common`, `shared`, or broad utility modules. New modules must name a durable ownership boundary.
|
|
115
|
-
|
|
116
|
-
## Single-Crate Decision
|
|
117
|
-
|
|
118
|
-
Planr remains a single crate for V1 because:
|
|
119
|
-
|
|
120
|
-
- one crate owns the CLI and its optional companion executables;
|
|
121
|
-
- the current behavior contract is tighter when CLI, MCP, HTTP, storage, and docs ship together;
|
|
122
|
-
- module-level ownership gives the needed architecture separation without duplicating Cargo settings or release packaging;
|
|
123
|
-
- npm, release, and external consumer tests treat `planr` as the product entry point. They also package provider-specific companion executables when Planr ships them.
|
|
124
|
-
|
|
125
|
-
A Cargo workspace was tried and reverted: it produced anemic crates whose only job was being a layer, plus re-export shims in the binary. A workspace should be introduced only after a concrete deployable, reuse, compilation, or team ownership boundary exists and package/release scripts are updated in the same change.
|
|
126
|
-
|
|
127
|
-
## Future Extract Points
|
|
128
|
-
|
|
129
|
-
If Planr grows past the V1 binary shape, the first clean extraction path is:
|
|
130
|
-
|
|
131
|
-
- `planr-core`: `model.rs`, graph invariants, plan package contracts, and pure use-case types.
|
|
132
|
-
- `planr-storage`: `src/storage/*`, storage repositories, schema upgrades, and import/export packages.
|
|
133
|
-
- `planr-cli`: `src/cli.rs`, human output, and install helpers.
|
|
134
|
-
- `planr-server`: `src/app/http.rs`, `src/app/mcp.rs`, and runtime server adapters.
|
|
135
|
-
|
|
136
|
-
Do not extract those crates until a real reuse, compile-time, or ownership boundary exists. Every extraction must leave a real owner with runtime code, tests, and one-way dependencies.
|