merge: wave 1 order 2 (intent) into main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
21c28df3bc
16 changed files with 1527 additions and 85 deletions
|
|
@ -6,7 +6,8 @@
|
|||
// optional fields (jf. the source_findings precedent on trekreview) can be
|
||||
// added without a brief_version bump.
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, ok, fail } from '../util/result.mjs';
|
||||
import { BASE_ALLOWED_MODELS } from './profile-validator.mjs';
|
||||
|
|
@ -24,6 +25,25 @@ export const BRIEF_FRAMING_VALUES = Object.freeze(['preserve', 'refine', 'replac
|
|||
// v5.5 — obligatory TL;DR section (≤ 5 content lines) at the top of brief.md,
|
||||
// gated at brief_version ≥ 2.2. Soft cap is a warning, not a blocker.
|
||||
export const BRIEF_TLDR_MAX_LINES = 5;
|
||||
// K1 — the brief's size class (ceremony by size). Optional, additive, no
|
||||
// brief_version bump; absent (or invalid) reads as the heaviest class.
|
||||
export const BRIEF_CEREMONY_VALUES = Object.freeze(['small', 'bounded', 'architectural']);
|
||||
export const BRIEF_CEREMONY_DEFAULT = 'architectural';
|
||||
// Where the brief and the plan live: committed (`git`) or kept out of git
|
||||
// (`local`). The approval log is committed either way. Optional, additive;
|
||||
// absent reads as `local`, so nothing is published without the operator's yes.
|
||||
export const BRIEF_ARTIFACT_STORE_VALUES = Object.freeze(['git', 'local']);
|
||||
export const BRIEF_ARTIFACT_STORE_DEFAULT = 'local';
|
||||
|
||||
/** The brief's size class; absent or invalid → architectural (never lighter in doubt). */
|
||||
export function briefCeremony(fm) {
|
||||
return BRIEF_CEREMONY_VALUES.includes(fm?.ceremony) ? fm.ceremony : BRIEF_CEREMONY_DEFAULT;
|
||||
}
|
||||
|
||||
/** Where the brief and plan are stored; absent or invalid → local. */
|
||||
export function briefArtifactStore(fm) {
|
||||
return BRIEF_ARTIFACT_STORE_VALUES.includes(fm?.artifact_store) ? fm.artifact_store : BRIEF_ARTIFACT_STORE_DEFAULT;
|
||||
}
|
||||
|
||||
// Extract the raw text of a `## {heading}` section body (between its heading line
|
||||
// and the next `## ` heading, or end of document). Returns null if absent.
|
||||
|
|
@ -115,6 +135,21 @@ export function validateBriefContent(text, opts = {}) {
|
|||
));
|
||||
}
|
||||
|
||||
if ('ceremony' in fm && !BRIEF_CEREMONY_VALUES.includes(fm.ceremony)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_CEREMONY',
|
||||
`ceremony "${fm.ceremony}" not in [${BRIEF_CEREMONY_VALUES.join(', ')}]`,
|
||||
'ceremony is the brief\'s size class; leave it out to mean architectural (the heaviest).',
|
||||
));
|
||||
}
|
||||
if ('artifact_store' in fm && !BRIEF_ARTIFACT_STORE_VALUES.includes(fm.artifact_store)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_ARTIFACT_STORE',
|
||||
`artifact_store "${fm.artifact_store}" not in [${BRIEF_ARTIFACT_STORE_VALUES.join(', ')}]`,
|
||||
'git = the brief and plan are committed; local = kept out of git. Leave it out to mean local.',
|
||||
));
|
||||
}
|
||||
|
||||
if (typeof fm.brief_version === 'string' || typeof fm.brief_version === 'number') {
|
||||
const vm = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
||||
if (vm) {
|
||||
|
|
@ -246,7 +281,15 @@ export function validateBrief(filePath, opts = {}) {
|
|||
return { ...r, parsed: { ...r.parsed, filePath } };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const minIdx = args.indexOf('--min-version');
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@
|
|||
// what `/trekbrief --approve` approves: {brief_path, project_dir, plan_command}; exit 1 = no brief
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue } from '../util/result.mjs';
|
||||
import { emit } from '../stats/event-emit.mjs';
|
||||
|
|
@ -233,7 +234,18 @@ export function resolveApproveTarget(arg) {
|
|||
return { brief_path: briefPath, project_dir: projectDir, plan_command: planCommand };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
// Main-module guard on real paths: comparing against `file://` + argv[1] fails
|
||||
// when the plugin sits behind a symlink or in a path with a space; the CLI then
|
||||
// exits 0 having checked nothing.
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find((a) => !a.startsWith('--'));
|
||||
if (args.includes('--resolve')) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue