obsidian-lean-coder 0.0.0-stage → 0.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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +153 -2
- package/agents/olc-build.md +47 -0
- package/agents/olc-plan.md +44 -0
- package/dist/approval/index.d.ts +53 -0
- package/dist/approval/index.js +123 -0
- package/dist/build/index.d.ts +52 -0
- package/dist/build/index.js +146 -0
- package/dist/config/index.d.ts +56 -0
- package/dist/config/index.js +136 -0
- package/dist/core/project-identity.d.ts +53 -0
- package/dist/core/project-identity.js +141 -0
- package/dist/git/checkpoint.d.ts +63 -0
- package/dist/git/checkpoint.js +123 -0
- package/dist/git/inspection.d.ts +35 -0
- package/dist/git/inspection.js +78 -0
- package/dist/git/policy.d.ts +65 -0
- package/dist/git/policy.js +88 -0
- package/dist/git/restore.d.ts +63 -0
- package/dist/git/restore.js +143 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/init/index.d.ts +45 -0
- package/dist/init/index.js +178 -0
- package/dist/log/index.d.ts +68 -0
- package/dist/log/index.js +224 -0
- package/dist/memory/index.d.ts +89 -0
- package/dist/memory/index.js +166 -0
- package/dist/planning/index.d.ts +57 -0
- package/dist/planning/index.js +180 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.js +46 -0
- package/dist/resume/index.d.ts +60 -0
- package/dist/resume/index.js +121 -0
- package/dist/session/index.d.ts +54 -0
- package/dist/session/index.js +108 -0
- package/dist/tasks/index.d.ts +59 -0
- package/dist/tasks/index.js +182 -0
- package/dist/taste/index.d.ts +79 -0
- package/dist/taste/index.js +162 -0
- package/dist/validation/index.d.ts +58 -0
- package/dist/validation/index.js +72 -0
- package/dist/vault/index.d.ts +50 -0
- package/dist/vault/index.js +153 -0
- package/dist/verify/index.d.ts +78 -0
- package/dist/verify/index.js +137 -0
- package/package.json +36 -4
- package/skills/olc-explore/SKILL.md +35 -0
- package/templates/.gitkeep +0 -0
- package/templates/activity-log.md +10 -0
- package/templates/checkpoint.md +24 -0
- package/templates/decision.md +19 -0
- package/templates/explore-report.md +23 -0
- package/templates/intent.md +23 -0
- package/templates/plan.md +27 -0
- package/templates/session-summary.md +26 -0
- package/templates/taste-rule.md +14 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start and pre-operation vault/project validation (OLC-065).
|
|
3
|
+
*
|
|
4
|
+
* Catches a missing/mismatched vault before OLC damages the wrong context.
|
|
5
|
+
* Lightweight by construction: every check reuses OLC-021 `validateIdentity`,
|
|
6
|
+
* which reads only git metadata (`rev-parse`) plus the single small
|
|
7
|
+
* `OLC/registry.json` file and a vault accessibility probe (`readdirSync`).
|
|
8
|
+
* The whole vault is never scanned.
|
|
9
|
+
*
|
|
10
|
+
* - `startSession` — call once at session start. Throws (IdentityError)
|
|
11
|
+
* before anything is read or written when the repo, vault, or registration
|
|
12
|
+
* is wrong.
|
|
13
|
+
* - `assertContextCurrent` — call before critical read/write/commit/restore
|
|
14
|
+
* operations. Re-runs the same cheap gate (catches a vault unmounted or
|
|
15
|
+
* a registry rewritten *between* operations) and additionally blocks on
|
|
16
|
+
* drift: the repo root or branch no longer matches the session start
|
|
17
|
+
* (branch switched mid-session), which always needs user confirmation.
|
|
18
|
+
*
|
|
19
|
+
* Identity problems propagate as `IdentityError` unchanged; drift is a
|
|
20
|
+
* `ValidationError(SESSION_DRIFT)`. Both block the operation with zero
|
|
21
|
+
* writes.
|
|
22
|
+
*/
|
|
23
|
+
import type { ResolvedConfig } from "../config/index.js";
|
|
24
|
+
export type ValidationErrorCode = "SESSION_DRIFT";
|
|
25
|
+
export declare class ValidationError extends Error {
|
|
26
|
+
readonly code: ValidationErrorCode;
|
|
27
|
+
constructor(code: ValidationErrorCode, message: string);
|
|
28
|
+
}
|
|
29
|
+
export interface SessionContext {
|
|
30
|
+
repoRoot: string;
|
|
31
|
+
branch: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
vaultRoot: string;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SessionStartInput {
|
|
37
|
+
/** Any directory inside the project (identity resolution starts here). */
|
|
38
|
+
projectRoot: string;
|
|
39
|
+
config: ResolvedConfig;
|
|
40
|
+
/** Defaults to current time (pass explicit dates in tests). */
|
|
41
|
+
date?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RevalidateInput extends SessionStartInput {
|
|
44
|
+
/** Context captured by `startSession`; drift against it blocks. */
|
|
45
|
+
session: SessionContext;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Session-start gate. Returns the validated context to keep and pass to
|
|
49
|
+
* `assertContextCurrent` before later critical operations.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startSession(input: SessionStartInput): SessionContext;
|
|
52
|
+
/**
|
|
53
|
+
* Pre-operation revalidation. Re-runs the cheap identity gate (a vault
|
|
54
|
+
* unmounted, deleted, or re-registered between operations throws here,
|
|
55
|
+
* before the critical op touches anything) and blocks when the repo root
|
|
56
|
+
* or branch drifted since session start.
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertContextCurrent(input: RevalidateInput): SessionContext;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start and pre-operation vault/project validation (OLC-065).
|
|
3
|
+
*
|
|
4
|
+
* Catches a missing/mismatched vault before OLC damages the wrong context.
|
|
5
|
+
* Lightweight by construction: every check reuses OLC-021 `validateIdentity`,
|
|
6
|
+
* which reads only git metadata (`rev-parse`) plus the single small
|
|
7
|
+
* `OLC/registry.json` file and a vault accessibility probe (`readdirSync`).
|
|
8
|
+
* The whole vault is never scanned.
|
|
9
|
+
*
|
|
10
|
+
* - `startSession` — call once at session start. Throws (IdentityError)
|
|
11
|
+
* before anything is read or written when the repo, vault, or registration
|
|
12
|
+
* is wrong.
|
|
13
|
+
* - `assertContextCurrent` — call before critical read/write/commit/restore
|
|
14
|
+
* operations. Re-runs the same cheap gate (catches a vault unmounted or
|
|
15
|
+
* a registry rewritten *between* operations) and additionally blocks on
|
|
16
|
+
* drift: the repo root or branch no longer matches the session start
|
|
17
|
+
* (branch switched mid-session), which always needs user confirmation.
|
|
18
|
+
*
|
|
19
|
+
* Identity problems propagate as `IdentityError` unchanged; drift is a
|
|
20
|
+
* `ValidationError(SESSION_DRIFT)`. Both block the operation with zero
|
|
21
|
+
* writes.
|
|
22
|
+
*/
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
|
+
import { validateIdentity } from "../core/project-identity.js";
|
|
25
|
+
export class ValidationError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ValidationError";
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Session-start gate. Returns the validated context to keep and pass to
|
|
35
|
+
* `assertContextCurrent` before later critical operations.
|
|
36
|
+
*/
|
|
37
|
+
export function startSession(input) {
|
|
38
|
+
const identity = validateIdentity(input.projectRoot, input.config);
|
|
39
|
+
return {
|
|
40
|
+
repoRoot: identity.repoRoot,
|
|
41
|
+
branch: identity.branch,
|
|
42
|
+
projectId: identity.projectId,
|
|
43
|
+
vaultRoot: identity.vaultRoot,
|
|
44
|
+
startedAt: input.date ?? new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Pre-operation revalidation. Re-runs the cheap identity gate (a vault
|
|
49
|
+
* unmounted, deleted, or re-registered between operations throws here,
|
|
50
|
+
* before the critical op touches anything) and blocks when the repo root
|
|
51
|
+
* or branch drifted since session start.
|
|
52
|
+
*/
|
|
53
|
+
export function assertContextCurrent(input) {
|
|
54
|
+
const now = validateIdentity(input.projectRoot, input.config);
|
|
55
|
+
if (resolve(now.repoRoot) !== resolve(input.session.repoRoot)) {
|
|
56
|
+
throw new ValidationError("SESSION_DRIFT", `Refusing operation: repository root changed since session start ` +
|
|
57
|
+
`(was ${input.session.repoRoot}, now ${now.repoRoot}). ` +
|
|
58
|
+
"Confirm which checkout this work belongs to before continuing — nothing was read or written.");
|
|
59
|
+
}
|
|
60
|
+
if (now.branch !== input.session.branch) {
|
|
61
|
+
throw new ValidationError("SESSION_DRIFT", `Refusing operation: branch changed since session start ` +
|
|
62
|
+
`(was ${input.session.branch}, now ${now.branch}). ` +
|
|
63
|
+
"Confirm which branch and task this work belongs to before continuing — nothing was switched or closed.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
repoRoot: now.repoRoot,
|
|
67
|
+
branch: now.branch,
|
|
68
|
+
projectId: now.projectId,
|
|
69
|
+
vaultRoot: now.vaultRoot,
|
|
70
|
+
startedAt: input.session.startedAt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown vault adapter (OLC-023).
|
|
3
|
+
*
|
|
4
|
+
* - `resolveVaultDoc`: safe path resolver — resolved paths must stay inside
|
|
5
|
+
* the project's vault scope (`<vaultRoot>/OLC/Projects/<projectId>/`).
|
|
6
|
+
* - Minimal frontmatter convention: flat `key: value` scalars only.
|
|
7
|
+
* - `writeVaultDoc`: atomic (temp file + rename); refuses to replace an
|
|
8
|
+
* existing document unless `overwrite: true` is passed explicitly.
|
|
9
|
+
* - `renderTemplate`: `{{variable}}` substitution over the package
|
|
10
|
+
* `templates/` directory; missing variables are an error, never silent.
|
|
11
|
+
*/
|
|
12
|
+
export declare class VaultError extends Error {
|
|
13
|
+
readonly code: "PATH_ESCAPE" | "ALREADY_EXISTS" | "TEMPLATE_ERROR";
|
|
14
|
+
constructor(code: "PATH_ESCAPE" | "ALREADY_EXISTS" | "TEMPLATE_ERROR", message: string);
|
|
15
|
+
}
|
|
16
|
+
/** Vault scope root for one project, e.g. `<vault>/OLC/Projects/<id>`. */
|
|
17
|
+
export declare function projectScopeRoot(vaultRoot: string, projectId: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve `...segments` inside the project's vault scope.
|
|
20
|
+
* Throws `VaultError(PATH_ESCAPE)` when the result would leave the scope.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveVaultDoc(vaultRoot: string, projectId: string, ...segments: string[]): string;
|
|
23
|
+
export interface FrontmatterDoc {
|
|
24
|
+
data: Record<string, string>;
|
|
25
|
+
body: string;
|
|
26
|
+
}
|
|
27
|
+
/** Parse `---\nkey: value\n---\nbody`. Flat string scalars only; no doc → empty data. */
|
|
28
|
+
export declare function parseFrontmatter(text: string): FrontmatterDoc;
|
|
29
|
+
/** Serialize flat string data + body with `---` fences. Values must be single-line. */
|
|
30
|
+
export declare function withFrontmatter(data: Record<string, string>, body: string): string;
|
|
31
|
+
export declare function readVaultDoc(path: string): string;
|
|
32
|
+
export interface WriteOptions {
|
|
33
|
+
/** Must be true to replace an existing document. Default false (never silent). */
|
|
34
|
+
overwrite?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Atomically write a UTF-8 vault document (temp file in the same directory
|
|
38
|
+
* + rename). Creates parent directories. Refuses to replace an existing file
|
|
39
|
+
* unless `overwrite: true`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeVaultDoc(path: string, content: string, options?: WriteOptions): void;
|
|
42
|
+
/** Locate the package `templates/` directory from the running module. */
|
|
43
|
+
export declare function findTemplatesDir(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Render a `templates/<name>.md` template with `{{variable}}` substitution.
|
|
46
|
+
* Every placeholder must have a value; leftovers are an error.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderTemplate(name: string, vars: Record<string, string>): string;
|
|
49
|
+
/** Template names shipped in `templates/` (one per OLC-023 work item). */
|
|
50
|
+
export declare const BUNDLED_TEMPLATES: readonly ["intent", "plan", "explore-report", "checkpoint", "activity-log", "session-summary", "decision", "taste-rule"];
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown vault adapter (OLC-023).
|
|
3
|
+
*
|
|
4
|
+
* - `resolveVaultDoc`: safe path resolver — resolved paths must stay inside
|
|
5
|
+
* the project's vault scope (`<vaultRoot>/OLC/Projects/<projectId>/`).
|
|
6
|
+
* - Minimal frontmatter convention: flat `key: value` scalars only.
|
|
7
|
+
* - `writeVaultDoc`: atomic (temp file + rename); refuses to replace an
|
|
8
|
+
* existing document unless `overwrite: true` is passed explicitly.
|
|
9
|
+
* - `renderTemplate`: `{{variable}}` substitution over the package
|
|
10
|
+
* `templates/` directory; missing variables are an error, never silent.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
export class VaultError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "VaultError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Vault scope root for one project, e.g. `<vault>/OLC/Projects/<id>`. */
|
|
24
|
+
export function projectScopeRoot(vaultRoot, projectId) {
|
|
25
|
+
return resolve(vaultRoot, "OLC", "Projects", projectId);
|
|
26
|
+
}
|
|
27
|
+
function checkSegment(segment) {
|
|
28
|
+
if (segment === "" ||
|
|
29
|
+
segment === "." ||
|
|
30
|
+
segment === ".." ||
|
|
31
|
+
isAbsolute(segment) ||
|
|
32
|
+
segment.includes("\0")) {
|
|
33
|
+
throw new VaultError("PATH_ESCAPE", `Refused vault path segment ${JSON.stringify(segment)}: ` +
|
|
34
|
+
"segments must be plain relative names, never absolute or parent references.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve `...segments` inside the project's vault scope.
|
|
39
|
+
* Throws `VaultError(PATH_ESCAPE)` when the result would leave the scope.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveVaultDoc(vaultRoot, projectId, ...segments) {
|
|
42
|
+
const scope = projectScopeRoot(vaultRoot, projectId);
|
|
43
|
+
for (const segment of segments) {
|
|
44
|
+
for (const part of segment.split("/"))
|
|
45
|
+
checkSegment(part);
|
|
46
|
+
}
|
|
47
|
+
const resolved = resolve(scope, ...segments);
|
|
48
|
+
if (resolved !== scope && !resolved.startsWith(scope + sep)) {
|
|
49
|
+
throw new VaultError("PATH_ESCAPE", `Refused vault path ${JSON.stringify(segments.join("/"))}: resolves outside the project scope.`);
|
|
50
|
+
}
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
/** Parse `---\nkey: value\n---\nbody`. Flat string scalars only; no doc → empty data. */
|
|
54
|
+
export function parseFrontmatter(text) {
|
|
55
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
56
|
+
if (!match)
|
|
57
|
+
return { data: {}, body: text };
|
|
58
|
+
const data = {};
|
|
59
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
60
|
+
if (line.trim() === "")
|
|
61
|
+
continue;
|
|
62
|
+
const colon = line.indexOf(":");
|
|
63
|
+
if (colon === -1)
|
|
64
|
+
continue;
|
|
65
|
+
data[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
66
|
+
}
|
|
67
|
+
return { data, body: match[2] };
|
|
68
|
+
}
|
|
69
|
+
/** Serialize flat string data + body with `---` fences. Values must be single-line. */
|
|
70
|
+
export function withFrontmatter(data, body) {
|
|
71
|
+
for (const [key, value] of Object.entries(data)) {
|
|
72
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key) || value.includes("\n")) {
|
|
73
|
+
throw new VaultError("TEMPLATE_ERROR", `Cannot serialize frontmatter key ${JSON.stringify(key)}: keys must be [A-Za-z0-9_-] and values single-line.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const head = Object.entries(data)
|
|
77
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
78
|
+
.join("\n");
|
|
79
|
+
return `---\n${head}\n---\n${body}`;
|
|
80
|
+
}
|
|
81
|
+
export function readVaultDoc(path) {
|
|
82
|
+
return readFileSync(path, "utf8");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Atomically write a UTF-8 vault document (temp file in the same directory
|
|
86
|
+
* + rename). Creates parent directories. Refuses to replace an existing file
|
|
87
|
+
* unless `overwrite: true`.
|
|
88
|
+
*/
|
|
89
|
+
export function writeVaultDoc(path, content, options = {}) {
|
|
90
|
+
if (!options.overwrite && existsSync(path)) {
|
|
91
|
+
throw new VaultError("ALREADY_EXISTS", `Refused to overwrite existing vault document at ${path}: pass { overwrite: true } to replace it explicitly.`);
|
|
92
|
+
}
|
|
93
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
94
|
+
const tmp = join(dirname(path), `.olc-tmp-${process.pid}-${Date.now()}`);
|
|
95
|
+
writeFileSync(tmp, content, "utf8");
|
|
96
|
+
renameSync(tmp, path);
|
|
97
|
+
}
|
|
98
|
+
/** Locate the package `templates/` directory from the running module. */
|
|
99
|
+
export function findTemplatesDir() {
|
|
100
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
101
|
+
for (const candidate of [
|
|
102
|
+
resolve(here, "..", "templates"),
|
|
103
|
+
resolve(here, "..", "..", "templates"),
|
|
104
|
+
resolve(here, "..", "..", "..", "templates"),
|
|
105
|
+
]) {
|
|
106
|
+
if (existsSync(candidate))
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
throw new VaultError("TEMPLATE_ERROR", "Cannot locate the OLC templates/ directory relative to the running package. The installed package may be incomplete.");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Render a `templates/<name>.md` template with `{{variable}}` substitution.
|
|
113
|
+
* Every placeholder must have a value; leftovers are an error.
|
|
114
|
+
*/
|
|
115
|
+
export function renderTemplate(name, vars) {
|
|
116
|
+
if (name.includes("/") || name.includes("\\") || name.startsWith(".")) {
|
|
117
|
+
throw new VaultError("TEMPLATE_ERROR", `Refused template name ${JSON.stringify(name)}: plain file names only.`);
|
|
118
|
+
}
|
|
119
|
+
const path = join(findTemplatesDir(), `${name}.md`);
|
|
120
|
+
let text;
|
|
121
|
+
try {
|
|
122
|
+
text = readFileSync(path, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new VaultError("TEMPLATE_ERROR", `Template not found: ${name}.md.`);
|
|
126
|
+
}
|
|
127
|
+
const missing = new Set();
|
|
128
|
+
const rendered = text.replace(/\{\{\s*([A-Za-z0-9_-]+)\s*\}\}/g, (m, key) => {
|
|
129
|
+
if (!(key in vars)) {
|
|
130
|
+
missing.add(key);
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
133
|
+
return vars[key];
|
|
134
|
+
});
|
|
135
|
+
if (missing.size > 0) {
|
|
136
|
+
throw new VaultError("TEMPLATE_ERROR", `Template ${name}.md is missing values for: ${[...missing].join(", ")}.`);
|
|
137
|
+
}
|
|
138
|
+
if (/\{\{\s*[A-Za-z0-9_-]+\s*\}\}/.test(rendered)) {
|
|
139
|
+
throw new VaultError("TEMPLATE_ERROR", `Template ${name}.md has placeholders that accept no value (check for typos).`);
|
|
140
|
+
}
|
|
141
|
+
return rendered;
|
|
142
|
+
}
|
|
143
|
+
/** Template names shipped in `templates/` (one per OLC-023 work item). */
|
|
144
|
+
export const BUNDLED_TEMPLATES = [
|
|
145
|
+
"intent",
|
|
146
|
+
"plan",
|
|
147
|
+
"explore-report",
|
|
148
|
+
"checkpoint",
|
|
149
|
+
"activity-log",
|
|
150
|
+
"session-summary",
|
|
151
|
+
"decision",
|
|
152
|
+
"taste-rule",
|
|
153
|
+
];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OLC-041 — test/report/escalation behavior.
|
|
3
|
+
*
|
|
4
|
+
* Evidence-backed verification for one task frame:
|
|
5
|
+
* - `runCheck` executes a single command (no shell) and captures the real
|
|
6
|
+
* outcome. Outcomes are NEVER invented: a spawn error, timeout, or
|
|
7
|
+
* non-zero exit is recorded as-is and counts as failure.
|
|
8
|
+
* - `verifyChecks` re-runs failing checks up to `maxAttempts` (bounded
|
|
9
|
+
* retry) and records EVERY attempt, so the report shows what actually ran.
|
|
10
|
+
* - `completeTaskWithVerification` runs the checks first and only delegates
|
|
11
|
+
* to OLC-040 `completeTask` when all pass; otherwise it returns
|
|
12
|
+
* `verify-failed` and leaves the registry untouched (caller stops and
|
|
13
|
+
* reports — the escalation path). Scope/architecture changes still require
|
|
14
|
+
* plan revision via OLC-034; this module never widens scope by itself.
|
|
15
|
+
*/
|
|
16
|
+
import { type BuildFrame } from "../build/index.js";
|
|
17
|
+
import type { ResolvedConfig } from "../config/index.js";
|
|
18
|
+
export interface CheckSpec {
|
|
19
|
+
/** Human-readable name shown in the report (e.g. "unit tests"). */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Executable + args. Run directly with no shell. */
|
|
22
|
+
command: string[];
|
|
23
|
+
/** Per-attempt wall-clock budget. Default 120_000. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface CheckAttempt {
|
|
27
|
+
attempt: number;
|
|
28
|
+
/** Process exit code; null when the process never ran or was killed. */
|
|
29
|
+
exitCode: number | null;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
/** Human-readable spawn failure (e.g. ENOENT). Absent on normal runs. */
|
|
34
|
+
error?: string;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface CheckResult {
|
|
39
|
+
name: string;
|
|
40
|
+
command: string[];
|
|
41
|
+
ok: boolean;
|
|
42
|
+
attempts: CheckAttempt[];
|
|
43
|
+
}
|
|
44
|
+
export interface VerifyOutcome {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
results: CheckResult[];
|
|
47
|
+
/** Names of checks still failing after all attempts (the escalation list). */
|
|
48
|
+
escalated: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Execute one check attempt. Never throws for check failures; only for programmer errors. */
|
|
51
|
+
export declare function runCheck(spec: CheckSpec, cwd: string, attempt?: number): CheckAttempt;
|
|
52
|
+
/**
|
|
53
|
+
* Run every check, retrying failures up to `maxAttempts` total attempts per
|
|
54
|
+
* check (bounded: `maxAttempts: 1` means no retry). Every attempt is kept.
|
|
55
|
+
*/
|
|
56
|
+
export declare function verifyChecks(specs: CheckSpec[], cwd: string, options?: {
|
|
57
|
+
maxAttempts?: number;
|
|
58
|
+
}): VerifyOutcome;
|
|
59
|
+
/** Render the outcome as Markdown (for history.md / session reports). */
|
|
60
|
+
export declare function formatVerifyReport(outcome: VerifyOutcome): string;
|
|
61
|
+
export type CompleteWithVerificationResult = {
|
|
62
|
+
verified: true;
|
|
63
|
+
changed: string[];
|
|
64
|
+
} | {
|
|
65
|
+
verified: false;
|
|
66
|
+
status: "verify-failed";
|
|
67
|
+
outcome: VerifyOutcome;
|
|
68
|
+
report: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Verify first, complete only on pass. On failure the task registry is left
|
|
72
|
+
* untouched (still in_progress) and the caller must stop + report the
|
|
73
|
+
* outcome (escalation) instead of claiming success.
|
|
74
|
+
*/
|
|
75
|
+
export declare function completeTaskWithVerification(frame: BuildFrame, config: ResolvedConfig, repoRoot: string, checks: CheckSpec[], options?: {
|
|
76
|
+
maxAttempts?: number;
|
|
77
|
+
date?: string;
|
|
78
|
+
}): CompleteWithVerificationResult;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OLC-041 — test/report/escalation behavior.
|
|
3
|
+
*
|
|
4
|
+
* Evidence-backed verification for one task frame:
|
|
5
|
+
* - `runCheck` executes a single command (no shell) and captures the real
|
|
6
|
+
* outcome. Outcomes are NEVER invented: a spawn error, timeout, or
|
|
7
|
+
* non-zero exit is recorded as-is and counts as failure.
|
|
8
|
+
* - `verifyChecks` re-runs failing checks up to `maxAttempts` (bounded
|
|
9
|
+
* retry) and records EVERY attempt, so the report shows what actually ran.
|
|
10
|
+
* - `completeTaskWithVerification` runs the checks first and only delegates
|
|
11
|
+
* to OLC-040 `completeTask` when all pass; otherwise it returns
|
|
12
|
+
* `verify-failed` and leaves the registry untouched (caller stops and
|
|
13
|
+
* reports — the escalation path). Scope/architecture changes still require
|
|
14
|
+
* plan revision via OLC-034; this module never widens scope by itself.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { completeTask } from "../build/index.js";
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
19
|
+
const MAX_OUTPUT_CHARS = 20_000;
|
|
20
|
+
function clip(s) {
|
|
21
|
+
return s.length > MAX_OUTPUT_CHARS ? s.slice(0, MAX_OUTPUT_CHARS) + "\n…(truncated)" : s;
|
|
22
|
+
}
|
|
23
|
+
/** Execute one check attempt. Never throws for check failures; only for programmer errors. */
|
|
24
|
+
export function runCheck(spec, cwd, attempt = 1) {
|
|
25
|
+
const started = Date.now();
|
|
26
|
+
const base = {
|
|
27
|
+
attempt,
|
|
28
|
+
exitCode: null,
|
|
29
|
+
stdout: "",
|
|
30
|
+
stderr: "",
|
|
31
|
+
timedOut: false,
|
|
32
|
+
};
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
const [cmd, ...args] = spec.command;
|
|
36
|
+
if (!cmd) {
|
|
37
|
+
result = { ...base, error: "empty command" };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const spawned = spawnSync(cmd, args, {
|
|
41
|
+
cwd,
|
|
42
|
+
timeout: spec.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
45
|
+
shell: false,
|
|
46
|
+
});
|
|
47
|
+
const err = spawned.error;
|
|
48
|
+
if (err) {
|
|
49
|
+
result = {
|
|
50
|
+
...base,
|
|
51
|
+
stdout: clip(String(spawned.stdout ?? "")),
|
|
52
|
+
stderr: clip(String(spawned.stderr ?? "")),
|
|
53
|
+
timedOut: err.code === "ETIMEDOUT",
|
|
54
|
+
error: `${err.code ?? err.name}: ${err.message}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
result = {
|
|
59
|
+
...base,
|
|
60
|
+
exitCode: spawned.status,
|
|
61
|
+
stdout: clip(String(spawned.stdout ?? "")),
|
|
62
|
+
stderr: clip(String(spawned.stderr ?? "")),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
result = { ...base, error: `runner error: ${error.message}` };
|
|
69
|
+
}
|
|
70
|
+
const durationMs = Date.now() - started;
|
|
71
|
+
return { ...result, durationMs, ok: result.exitCode === 0 && !result.error };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run every check, retrying failures up to `maxAttempts` total attempts per
|
|
75
|
+
* check (bounded: `maxAttempts: 1` means no retry). Every attempt is kept.
|
|
76
|
+
*/
|
|
77
|
+
export function verifyChecks(specs, cwd, options) {
|
|
78
|
+
const maxAttempts = Math.max(1, Math.floor(options?.maxAttempts ?? 1));
|
|
79
|
+
const results = specs.map((spec) => {
|
|
80
|
+
const attempts = [runCheck(spec, cwd, 1)];
|
|
81
|
+
while (!attempts[attempts.length - 1].ok && attempts.length < maxAttempts) {
|
|
82
|
+
attempts.push(runCheck(spec, cwd, attempts.length + 1));
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: spec.name,
|
|
86
|
+
command: spec.command,
|
|
87
|
+
ok: attempts[attempts.length - 1].ok,
|
|
88
|
+
attempts,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
ok: results.every((r) => r.ok),
|
|
93
|
+
results,
|
|
94
|
+
escalated: results.filter((r) => !r.ok).map((r) => r.name),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Render the outcome as Markdown (for history.md / session reports). */
|
|
98
|
+
export function formatVerifyReport(outcome) {
|
|
99
|
+
const lines = [
|
|
100
|
+
"## verification",
|
|
101
|
+
"",
|
|
102
|
+
`Result: ${outcome.ok ? "PASS" : "FAIL"} (${outcome.results.filter((r) => r.ok).length}/${outcome.results.length} checks passed)`,
|
|
103
|
+
"",
|
|
104
|
+
];
|
|
105
|
+
for (const r of outcome.results) {
|
|
106
|
+
lines.push(`### ${r.name} — ${r.ok ? "PASS" : "FAIL"} (${r.attempts.length} attempt(s))`);
|
|
107
|
+
lines.push(`\`$ ${r.command.join(" ")}\``);
|
|
108
|
+
for (const a of r.attempts) {
|
|
109
|
+
lines.push(`- attempt ${a.attempt}: exit=${a.exitCode === null ? "n/a" : a.exitCode}` +
|
|
110
|
+
(a.timedOut ? " TIMED OUT" : "") +
|
|
111
|
+
(a.error ? ` error=${a.error}` : "") +
|
|
112
|
+
` (${a.durationMs}ms)`);
|
|
113
|
+
if (a.stdout.trim() !== "")
|
|
114
|
+
lines.push(` stdout: ${a.stdout.trim().split("\n").slice(-5).join(" | ")}`);
|
|
115
|
+
if (a.stderr.trim() !== "")
|
|
116
|
+
lines.push(` stderr: ${a.stderr.trim().split("\n").slice(-5).join(" | ")}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
if (!outcome.ok) {
|
|
121
|
+
lines.push(`Escalated (needs user guidance, no auto-retry left): ${outcome.escalated.join(", ")}`);
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Verify first, complete only on pass. On failure the task registry is left
|
|
128
|
+
* untouched (still in_progress) and the caller must stop + report the
|
|
129
|
+
* outcome (escalation) instead of claiming success.
|
|
130
|
+
*/
|
|
131
|
+
export function completeTaskWithVerification(frame, config, repoRoot, checks, options) {
|
|
132
|
+
const outcome = verifyChecks(checks, repoRoot, { maxAttempts: options?.maxAttempts });
|
|
133
|
+
if (!outcome.ok) {
|
|
134
|
+
return { verified: false, status: "verify-failed", outcome, report: formatVerifyReport(outcome) };
|
|
135
|
+
}
|
|
136
|
+
return { verified: true, ...completeTask(frame, config, repoRoot, options?.date) };
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-lean-coder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Obsidian Lean Coder — OpenCode extension for project-scoped planning, memory, and safe Git checkpoints.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"agents",
|
|
17
|
+
"skills",
|
|
18
|
+
"templates",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.18.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "tsc -p tsconfig.tests.json && node --test \"dist-tests/tests/**/*.test.js\"",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.6.3"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@opencode/plugin": "^2.0.27"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: olc-explore
|
|
3
|
+
description: Selidiki request yang kompleks/tidak pasti sebelum planning. Read-only terhadap app code; menghasilkan satu laporan eksplorasi di vault.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# olc-explore
|
|
7
|
+
|
|
8
|
+
Klarifikasi request yang kompleks atau tidak pasti **sebelum** `intent.md`/`plan.md` dibuat.
|
|
9
|
+
Untuk task sederhana yang sudah jelas, lewati skill ini dan langsung ke Plan (direct Plan).
|
|
10
|
+
|
|
11
|
+
## Kapan dipakai vs direct Plan
|
|
12
|
+
|
|
13
|
+
- Pakai Explore bila: request ambigu, menyentuh area yang belum dikenal, ada beberapa pendekatan dengan tradeoff nyata, atau risiko salah paham tinggi.
|
|
14
|
+
- Direct Plan bila: perubahan kecil dan terlokalisasi, pendekatan sudah jelas, tidak ada pertanyaan material yang terbuka.
|
|
15
|
+
- Pilihan user yang menang: kalau user minta direct Plan, jangan memaksa Explore, dan sebaliknya.
|
|
16
|
+
|
|
17
|
+
## Aturan keras
|
|
18
|
+
|
|
19
|
+
1. **Read-only terhadap app source.** Boleh membaca file, menjalankan perintah baca (status, log, test baca), menelusuri kode. DILARANG mengubah, menambah, atau menghapus file app dalam bentuk apa pun — tidak ada fix "sekalian", tidak ada refactor kecil.
|
|
20
|
+
2. Satu laporan aktif per task: `tasks/<task-id>/explore/report.md` di vault proyek yang sudah tervalidasi (OLC-021). Jangan membuat file laporan tandingan.
|
|
21
|
+
3. Setiap klaim temuan harus punya bukti: path file + baris, output perintah, atau kutipan config. Tanpa bukti = pertanyaan terbuka, bukan temuan.
|
|
22
|
+
4. Revisi tidak menghapus penalaran penting: revisi ditambahkan sebagai bagian baru bertanggal di laporan yang sama; alasan perubahan pendekatan harus tercatat.
|
|
23
|
+
|
|
24
|
+
## Alur
|
|
25
|
+
|
|
26
|
+
1. Validasi identitas proyek dan baca `project_id` dari `.olc/config.json` (jangan operasi vault sebelum ini lolos).
|
|
27
|
+
2. Baca request user + konteks relevan secukupnya (index/current-state vault bila ada, file app terkait). Jangan membaca seluruh vault.
|
|
28
|
+
3. Tulis laporan via template `explore-report` (var: `task_id`, `project_id`, `date`, `request`, `findings`, `options`, `recommendation`). Buat direktori `explore/` bila belum ada; jangan menimpa laporan yang sudah dikustom tanpa persetujuan (ikuti aturan create-only OLC-023).
|
|
29
|
+
4. Bila request berubah atau temuan baru muncul: tambah bagian revisi bertanggal di laporan yang sama, catat alasan di `history.md` task.
|
|
30
|
+
5. Serahkan ke Plan: laporan explore adalah input `draftTaskDocs`, bukan pengganti `intent.md`/`plan.md`. Tanpa kedua dokumen itu disetujui, tidak ada coding (OLC-032).
|
|
31
|
+
|
|
32
|
+
## Verifikasi skill ini
|
|
33
|
+
|
|
34
|
+
- Setelah Explore: `git status --short` di repo app harus bersih (tidak ada perubahan app).
|
|
35
|
+
- Laporan ada di path yang benar dan semua var template terisi (tidak ada `{{...}}` tersisa).
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
task: {{task_id}}
|
|
3
|
+
project: {{project_id}}
|
|
4
|
+
commit: {{commit_sha}}
|
|
5
|
+
date: {{date}}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Checkpoint — {{task_id}}
|
|
9
|
+
|
|
10
|
+
## Commit
|
|
11
|
+
|
|
12
|
+
{{commit_sha}}
|
|
13
|
+
|
|
14
|
+
## Summary (ringkasan perubahan)
|
|
15
|
+
|
|
16
|
+
{{summary}}
|
|
17
|
+
|
|
18
|
+
## Verification results
|
|
19
|
+
|
|
20
|
+
{{verification}}
|
|
21
|
+
|
|
22
|
+
## Timestamp
|
|
23
|
+
|
|
24
|
+
{{date}}
|