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.
Files changed (58) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -2
  4. package/agents/olc-build.md +47 -0
  5. package/agents/olc-plan.md +44 -0
  6. package/dist/approval/index.d.ts +53 -0
  7. package/dist/approval/index.js +123 -0
  8. package/dist/build/index.d.ts +52 -0
  9. package/dist/build/index.js +146 -0
  10. package/dist/config/index.d.ts +56 -0
  11. package/dist/config/index.js +136 -0
  12. package/dist/core/project-identity.d.ts +53 -0
  13. package/dist/core/project-identity.js +141 -0
  14. package/dist/git/checkpoint.d.ts +63 -0
  15. package/dist/git/checkpoint.js +123 -0
  16. package/dist/git/inspection.d.ts +35 -0
  17. package/dist/git/inspection.js +78 -0
  18. package/dist/git/policy.d.ts +65 -0
  19. package/dist/git/policy.js +88 -0
  20. package/dist/git/restore.d.ts +63 -0
  21. package/dist/git/restore.js +143 -0
  22. package/dist/index.d.ts +14 -0
  23. package/dist/index.js +14 -0
  24. package/dist/init/index.d.ts +45 -0
  25. package/dist/init/index.js +178 -0
  26. package/dist/log/index.d.ts +68 -0
  27. package/dist/log/index.js +224 -0
  28. package/dist/memory/index.d.ts +89 -0
  29. package/dist/memory/index.js +166 -0
  30. package/dist/planning/index.d.ts +57 -0
  31. package/dist/planning/index.js +180 -0
  32. package/dist/plugin.d.ts +16 -0
  33. package/dist/plugin.js +46 -0
  34. package/dist/resume/index.d.ts +60 -0
  35. package/dist/resume/index.js +121 -0
  36. package/dist/session/index.d.ts +54 -0
  37. package/dist/session/index.js +108 -0
  38. package/dist/tasks/index.d.ts +59 -0
  39. package/dist/tasks/index.js +182 -0
  40. package/dist/taste/index.d.ts +79 -0
  41. package/dist/taste/index.js +162 -0
  42. package/dist/validation/index.d.ts +58 -0
  43. package/dist/validation/index.js +72 -0
  44. package/dist/vault/index.d.ts +50 -0
  45. package/dist/vault/index.js +153 -0
  46. package/dist/verify/index.d.ts +78 -0
  47. package/dist/verify/index.js +137 -0
  48. package/package.json +36 -4
  49. package/skills/olc-explore/SKILL.md +35 -0
  50. package/templates/.gitkeep +0 -0
  51. package/templates/activity-log.md +10 -0
  52. package/templates/checkpoint.md +24 -0
  53. package/templates/decision.md +19 -0
  54. package/templates/explore-report.md +23 -0
  55. package/templates/intent.md +23 -0
  56. package/templates/plan.md +27 -0
  57. package/templates/session-summary.md +26 -0
  58. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,10 @@
1
+ ---
2
+ project: {{project_id}}
3
+ date: {{date}}
4
+ ---
5
+
6
+ # Activity log — {{date}}
7
+
8
+ ## {{event}} ({{timestamp}})
9
+
10
+ {{details}}
@@ -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}}