aiki-cli 0.2.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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/bench/arms.js +104 -0
- package/dist/bench/harness.js +251 -0
- package/dist/bench/results.js +70 -0
- package/dist/bench/scoring/seeded-bugs.js +31 -0
- package/dist/cli/bench.js +50 -0
- package/dist/cli/config.js +51 -0
- package/dist/cli/doctor.js +115 -0
- package/dist/cli/index.js +129 -0
- package/dist/cli/models.js +51 -0
- package/dist/cli/providers.js +31 -0
- package/dist/cli/resolve.js +159 -0
- package/dist/cli/resume.js +94 -0
- package/dist/cli/run.js +155 -0
- package/dist/cli/sessions.js +35 -0
- package/dist/cli/show.js +73 -0
- package/dist/config/config.js +102 -0
- package/dist/config/smoke-cache.js +65 -0
- package/dist/council/open.js +26 -0
- package/dist/council/view.js +873 -0
- package/dist/orchestration/cluster.js +83 -0
- package/dist/orchestration/context.js +277 -0
- package/dist/orchestration/engine.js +92 -0
- package/dist/orchestration/git.js +133 -0
- package/dist/orchestration/jsonStage.js +32 -0
- package/dist/orchestration/skills.js +39 -0
- package/dist/orchestration/stages/cr-ladder.js +63 -0
- package/dist/orchestration/stages/cr-map.js +62 -0
- package/dist/orchestration/stages/cr-report.js +83 -0
- package/dist/orchestration/stages/cr-s4-review.js +69 -0
- package/dist/orchestration/stages/cr-s8-crossexam.js +104 -0
- package/dist/orchestration/stages/cr-s9-judge.js +89 -0
- package/dist/orchestration/stages/s0-grill.js +79 -0
- package/dist/orchestration/stages/s1-intent.js +25 -0
- package/dist/orchestration/stages/s10-render.js +198 -0
- package/dist/orchestration/stages/s2-misread.js +76 -0
- package/dist/orchestration/stages/s3-prompts.js +55 -0
- package/dist/orchestration/stages/s4-analyze.js +50 -0
- package/dist/orchestration/stages/s5-drift.js +40 -0
- package/dist/orchestration/stages/s6-claims.js +56 -0
- package/dist/orchestration/stages/s7-disagreement.js +134 -0
- package/dist/orchestration/stages/s8-verify.js +56 -0
- package/dist/orchestration/stages/s9-judge.js +152 -0
- package/dist/orchestration/stages/s9b-plan.js +192 -0
- package/dist/providers/adapter-core.js +131 -0
- package/dist/providers/adapters.js +9 -0
- package/dist/providers/agy.js +29 -0
- package/dist/providers/claude.js +56 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/detect.js +21 -0
- package/dist/providers/probe.js +43 -0
- package/dist/providers/profiles.js +38 -0
- package/dist/providers/profiles.json +5 -0
- package/dist/providers/smoke.js +26 -0
- package/dist/providers/spawn.js +152 -0
- package/dist/providers/types.js +17 -0
- package/dist/schemas/index.js +374 -0
- package/dist/skills/.gitkeep +0 -0
- package/dist/skills/code-review/judge.md +23 -0
- package/dist/skills/code-review/reviewer.md +38 -0
- package/dist/skills/idea-refinement/analyst.md +45 -0
- package/dist/skills/idea-refinement/planner.md +25 -0
- package/dist/storage/feedback.js +111 -0
- package/dist/storage/paths.js +20 -0
- package/dist/storage/replay.js +0 -0
- package/dist/storage/runs-read.js +95 -0
- package/dist/storage/runs.js +129 -0
- package/dist/storage/sessions.js +71 -0
- package/dist/tui/app.js +444 -0
- package/dist/tui/format.js +27 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/smart-entry.js +106 -0
- package/dist/tui/timeline.js +91 -0
- package/dist/workflows/code-review.js +76 -0
- package/dist/workflows/idea-refinement.js +105 -0
- package/package.json +64 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Core shared zod schemas (§14). Single source of truth for every stage-boundary payload.
|
|
2
|
+
//
|
|
3
|
+
// Design rules honored here:
|
|
4
|
+
// - Keep small; every optional field justifies itself (§14).
|
|
5
|
+
// - Model-facing outputs are `.strict()`: unknown keys = validation failure → §14 repair retry.
|
|
6
|
+
// This is an anti-slop mechanic (§11): prose/extra fields cannot leak across a stage boundary.
|
|
7
|
+
// - Hard list caps come straight from the plan (§12/§13); verbosity cannot impersonate rigor (§11).
|
|
8
|
+
// - Internal composites we build ourselves (DisagreementMap, RunMeta) are NOT strict — we own them.
|
|
9
|
+
//
|
|
10
|
+
// NOT done here (deferred, out of T4 scope): §14's "export zod → skills/*/output.schema.json"
|
|
11
|
+
// JSON-Schema generation. It needs a new dep and belongs with the skills system (T5+).
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
// ── Enums shared across schemas ─────────────────────────────────────────────
|
|
14
|
+
/** Provider ids (mirrors providers/types.ts PROVIDER_IDS; `agy` = Antigravity/Gemini 3.1 Pro). */
|
|
15
|
+
export const ProviderIdSchema = z.enum(['claude', 'codex', 'agy']);
|
|
16
|
+
/** What S1 classifies the request as. `other` is a valid contract but not a runnable workflow. */
|
|
17
|
+
export const TaskTypeSchema = z.enum(['idea-refinement', 'code-review', 'other']);
|
|
18
|
+
/** The two runnable v1 workflows (§12). Discriminates RoleOutput and tags RunMeta. */
|
|
19
|
+
export const WorkflowIdSchema = z.enum(['idea-refinement', 'code-review']);
|
|
20
|
+
// ── S1: IntentContract (§13) ────────────────────────────────────────────────
|
|
21
|
+
export const IntentContract = z
|
|
22
|
+
.object({
|
|
23
|
+
task: z.string().min(1), // one-paragraph normalized restatement
|
|
24
|
+
task_type: TaskTypeSchema,
|
|
25
|
+
constraints: z.array(z.string()), // explicit constraints the user stated (may be empty)
|
|
26
|
+
unknowns: z.array(z.string()), // things the request leaves unspecified
|
|
27
|
+
success_criteria: z.array(z.string()), // what a good final output must contain
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
// ── S2: Interpretation — per provider (§13) ─────────────────────────────────
|
|
31
|
+
export const Interpretation = z
|
|
32
|
+
.object({
|
|
33
|
+
my_interpretation: z.string().min(1), // one sentence: what the model believes the user wants
|
|
34
|
+
plausible_misreadings: z.array(z.string()).max(2), // "top-2" (§13); empty is degenerate but legal
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
// ── S0: RunBrief / contextual grill ─────────────────────────────────────────
|
|
38
|
+
export const GrillQuestionAxis = z.enum([
|
|
39
|
+
'decision_frame',
|
|
40
|
+
'evaluation_lens',
|
|
41
|
+
'target_user',
|
|
42
|
+
'success_bar',
|
|
43
|
+
'non_negotiables',
|
|
44
|
+
'risk_context',
|
|
45
|
+
'evidence',
|
|
46
|
+
'alternatives',
|
|
47
|
+
'scope',
|
|
48
|
+
]);
|
|
49
|
+
export const RunBriefQuestion = z
|
|
50
|
+
.object({
|
|
51
|
+
id: z.string().min(1),
|
|
52
|
+
axis: GrillQuestionAxis,
|
|
53
|
+
question: z.string().min(1),
|
|
54
|
+
why_it_matters: z.string().min(1),
|
|
55
|
+
suggested_answers: z.array(z.string().min(1)).min(2).max(5),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
const RunBriefDraftBase = z
|
|
59
|
+
.object({
|
|
60
|
+
subject: z.string().min(1),
|
|
61
|
+
decision_frame: z.string().min(1).nullable(),
|
|
62
|
+
evaluation_lens: z.string().min(1).nullable(),
|
|
63
|
+
target_user: z.string().min(1).nullable(),
|
|
64
|
+
constraints: z.array(z.string().min(1)).max(10),
|
|
65
|
+
claims_to_test: z.array(z.string().min(1)).max(8),
|
|
66
|
+
evidence_supplied: z.array(z.string().min(1)).max(8),
|
|
67
|
+
missing_axes: z.array(z.string().min(1)).max(8),
|
|
68
|
+
questions: z.array(RunBriefQuestion).min(3).max(4),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
function checkQuestionIds(questions, ctx) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const q of questions) {
|
|
74
|
+
if (seen.has(q.id)) {
|
|
75
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['questions'], message: `duplicate question id: ${q.id}` });
|
|
76
|
+
}
|
|
77
|
+
seen.add(q.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => checkQuestionIds(brief.questions, ctx));
|
|
81
|
+
export const GrillAnswer = z
|
|
82
|
+
.object({
|
|
83
|
+
question_id: z.string().min(1),
|
|
84
|
+
answer: z.string().min(1),
|
|
85
|
+
source: z.enum(['user', 'suggested', 'default']),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
export const RunBrief = RunBriefDraftBase.extend({
|
|
89
|
+
answers: z.array(GrillAnswer).min(3).max(4),
|
|
90
|
+
}).superRefine((brief, ctx) => {
|
|
91
|
+
checkQuestionIds(brief.questions, ctx);
|
|
92
|
+
const questionIds = new Set(brief.questions.map((q) => q.id));
|
|
93
|
+
const answerIds = new Set();
|
|
94
|
+
for (const answer of brief.answers) {
|
|
95
|
+
if (!questionIds.has(answer.question_id)) {
|
|
96
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['answers'], message: `answer for unknown question id: ${answer.question_id}` });
|
|
97
|
+
}
|
|
98
|
+
if (answerIds.has(answer.question_id)) {
|
|
99
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['answers'], message: `duplicate answer for question id: ${answer.question_id}` });
|
|
100
|
+
}
|
|
101
|
+
answerIds.add(answer.question_id);
|
|
102
|
+
}
|
|
103
|
+
for (const q of brief.questions) {
|
|
104
|
+
if (!answerIds.has(q.id)) {
|
|
105
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['answers'], message: `missing answer for question id: ${q.id}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// ── S4: RoleOutput — workflow-discriminated union (§12, §13) ─────────────────
|
|
110
|
+
//
|
|
111
|
+
// The model output (§13) does NOT carry a `workflow` field; the engine injects the discriminator
|
|
112
|
+
// before `.parse()` (S4, T5): `RoleOutput.parse({ workflow, ...modelJson })`.
|
|
113
|
+
const Assumption = z
|
|
114
|
+
.object({
|
|
115
|
+
id: z.string().min(1), // "A1", "A2", ...
|
|
116
|
+
statement: z.string().min(1),
|
|
117
|
+
type: z.enum(['VERIFIABLE', 'JUDGMENT']),
|
|
118
|
+
load_bearing: z.boolean(),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
const Attack = z
|
|
122
|
+
.object({
|
|
123
|
+
id: z.string().min(1), // "X1", ...
|
|
124
|
+
target_assumption: z.string().min(1), // MUST reference an assumption id (validator enforces at T7)
|
|
125
|
+
argument: z.string().min(1),
|
|
126
|
+
severity: z.enum(['HIGH', 'MED', 'LOW']),
|
|
127
|
+
})
|
|
128
|
+
.strict();
|
|
129
|
+
export const IdeaRoleOutput = z
|
|
130
|
+
.object({
|
|
131
|
+
workflow: z.literal('idea-refinement'),
|
|
132
|
+
task_echo: z.string().min(1), // ≤2 sentence restatement (drift check, S5)
|
|
133
|
+
strongest_version: z.string().min(1), // ≤150 words
|
|
134
|
+
assumptions: z.array(Assumption).max(8),
|
|
135
|
+
attacks: z.array(Attack).max(6),
|
|
136
|
+
open_questions: z.array(z.string()).max(5),
|
|
137
|
+
})
|
|
138
|
+
.strict();
|
|
139
|
+
/** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
|
|
140
|
+
* is equality on this enum (off-by-one→CORRECTNESS, race→CONCURRENCY, unhandled-rejection→ERROR_HANDLING,
|
|
141
|
+
* auth-gap→SECURITY, N+1→PERF). */
|
|
142
|
+
export const FindingCategory = z.enum(['CORRECTNESS', 'SECURITY', 'CONCURRENCY', 'ERROR_HANDLING', 'PERF', 'MAINTAINABILITY']);
|
|
143
|
+
export const Finding = z
|
|
144
|
+
.object({
|
|
145
|
+
id: z.string().min(1), // "F1", ...
|
|
146
|
+
file: z.string().min(1),
|
|
147
|
+
line_start: z.number().int().nonnegative(),
|
|
148
|
+
line_end: z.number().int().nonnegative(),
|
|
149
|
+
severity: z.enum(['P0', 'P1', 'P2', 'P3']),
|
|
150
|
+
category: FindingCategory,
|
|
151
|
+
claim: z.string().min(1),
|
|
152
|
+
evidence: z.string().min(1), // the code/behavior that proves it
|
|
153
|
+
suggested_fix: z.string().min(1),
|
|
154
|
+
self_confidence: z.number().min(0).max(1),
|
|
155
|
+
})
|
|
156
|
+
.strict();
|
|
157
|
+
export const CodeReviewRoleOutput = z
|
|
158
|
+
.object({
|
|
159
|
+
workflow: z.literal('code-review'),
|
|
160
|
+
task_echo: z.string().min(1),
|
|
161
|
+
findings: z.array(Finding).max(12),
|
|
162
|
+
})
|
|
163
|
+
.strict();
|
|
164
|
+
export const RoleOutput = z.discriminatedUnion('workflow', [IdeaRoleOutput, CodeReviewRoleOutput]);
|
|
165
|
+
/** The exact JSON a code-review S4 reviewer returns: `CodeReviewRoleOutput` WITHOUT the `workflow`
|
|
166
|
+
* discriminator (§13 — model output carries no `workflow`). Mirrors `IdeaRoleOutputModel` (T6); S4
|
|
167
|
+
* validates the raw call against this, injects `workflow`, then persists as `RoleOutput` (T10). */
|
|
168
|
+
export const CodeReviewRoleOutputModel = CodeReviewRoleOutput.omit({ workflow: true });
|
|
169
|
+
/** The exact JSON the model returns for an idea-refinement S4 seat: `IdeaRoleOutput` WITHOUT the
|
|
170
|
+
* `workflow` discriminator (§13 — model output carries no `workflow`). S4 validates the raw call
|
|
171
|
+
* against this, then injects `workflow` and re-validates as `RoleOutput` before persisting.
|
|
172
|
+
* `.omit` preserves the object's strict mode, so extra keys still trigger the §14 repair retry. */
|
|
173
|
+
export const IdeaRoleOutputModel = IdeaRoleOutput.omit({ workflow: true });
|
|
174
|
+
// ── S3: StagePrompts (§9, §13) ──────────────────────────────────────────────
|
|
175
|
+
//
|
|
176
|
+
// S3 output: the role-specific S4 prompts with every {{SLOT}} filled. Deterministic validator
|
|
177
|
+
// (S3) additionally rejects any prompt still containing an unresolved `{{...}}` (§9 S3 row).
|
|
178
|
+
export const StagePrompts = z
|
|
179
|
+
.object({
|
|
180
|
+
prompts: z.record(z.string(), z.string()), // role name → filled prompt (non-empty map)
|
|
181
|
+
})
|
|
182
|
+
.strict();
|
|
183
|
+
// ── S7: ClaimGroups — semantic grouping call output (T7, decision B refined) ──
|
|
184
|
+
//
|
|
185
|
+
// The one constrained model call inside S7 (run on the judge role). It receives claim IDs +
|
|
186
|
+
// statements with attribution WITHHELD and returns ONLY groupings of existing IDs that mean the
|
|
187
|
+
// same thing. Strict + IDs-only is the anti-blending guard: the model groups by reference, never
|
|
188
|
+
// rewrites a claim. Empty `groups` = nothing merged (legal). Each group needs ≥2 IDs to be a merge.
|
|
189
|
+
export const ClaimGroups = z
|
|
190
|
+
.object({
|
|
191
|
+
groups: z.array(z.array(z.string().min(1)).min(2)),
|
|
192
|
+
})
|
|
193
|
+
.strict();
|
|
194
|
+
// ── S8: Verification (§13) ──────────────────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// `Verification` is the per-item verdict (§9 "per-item Verification"). `VerificationSet` is the
|
|
197
|
+
// actual S8 stage output: the array plus the mandatory justification when zero REFUTEs (§13).
|
|
198
|
+
export const Verification = z
|
|
199
|
+
.object({
|
|
200
|
+
target_id: z.string().min(1),
|
|
201
|
+
verdict: z.enum(['CONFIRM', 'REFUTE', 'UNCERTAIN']),
|
|
202
|
+
evidence: z.string().min(1), // the verifier's own independent evidence
|
|
203
|
+
note: z.string(), // ≤2 sentences
|
|
204
|
+
})
|
|
205
|
+
.strict();
|
|
206
|
+
export const VerificationSet = z
|
|
207
|
+
.object({
|
|
208
|
+
verifications: z.array(Verification),
|
|
209
|
+
// Required by the S8 prompt only when the verifier issued zero REFUTEs (§13).
|
|
210
|
+
all_confirmed_justification: z.string().optional(),
|
|
211
|
+
})
|
|
212
|
+
.strict();
|
|
213
|
+
// ── S7: DisagreementMap (§7, §9) ────────────────────────────────────────────
|
|
214
|
+
//
|
|
215
|
+
// The plan (§7/§9) names only the four arrays. `Claim` shape is from §6. NOTE two under-specified
|
|
216
|
+
// points resolved here as documented T4 choices, to be firmed when S6/S7 are built (T6/T7):
|
|
217
|
+
// 1. §6's snippet shows singular `provider`, but its prose says merged claims carry
|
|
218
|
+
// "multi-provider attribution" → we use `providers: []` (array) to hold that attribution.
|
|
219
|
+
// 2. The plan gives no element shape for `contradictions` → minimal {claim_ids, note?}.
|
|
220
|
+
// These are internal (engine-built), so this schema is not `.strict()`.
|
|
221
|
+
export const Claim = z.object({
|
|
222
|
+
id: z.string().min(1),
|
|
223
|
+
statement: z.string().min(1),
|
|
224
|
+
type: z.enum(['VERIFIABLE', 'JUDGMENT']),
|
|
225
|
+
providers: z.array(ProviderIdSchema).min(1), // attribution; ≥2 after an S6 merge
|
|
226
|
+
evidence: z.string().optional(),
|
|
227
|
+
});
|
|
228
|
+
// A dispute over a claim. For idea-refinement (T6) a contradiction is a contested assumption: one
|
|
229
|
+
// or more analysts asserted the claim, and ≥1 analyst attacked it. The `attacks` ARE the dispute
|
|
230
|
+
// content — they are exactly the "disputed items + evidence" the S8 verifier loop consumes (§9 S8),
|
|
231
|
+
// so a contradiction without attacks would be meaningless. `id` is the stable target S8/S9 reference.
|
|
232
|
+
// (Shape firmed at T6, as the T4 note anticipated: the old `claim_ids ≥2` assumed contradictions
|
|
233
|
+
// linked two claims, but the deterministic idea-refinement signal centers on one contested claim.)
|
|
234
|
+
export const Contradiction = z.object({
|
|
235
|
+
id: z.string().min(1), // "D1", ...
|
|
236
|
+
claim_ids: z.array(z.string()).min(1), // the contested claim id(s)
|
|
237
|
+
attacks: z
|
|
238
|
+
.array(z.object({
|
|
239
|
+
provider: ProviderIdSchema,
|
|
240
|
+
argument: z.string().min(1),
|
|
241
|
+
severity: z.enum(['HIGH', 'MED', 'LOW']),
|
|
242
|
+
}))
|
|
243
|
+
.min(1),
|
|
244
|
+
note: z.string().optional(),
|
|
245
|
+
});
|
|
246
|
+
export const DisagreementMap = z.object({
|
|
247
|
+
consensus: z.array(Claim), // agreed by ≥2 providers
|
|
248
|
+
contradictions: z.array(Contradiction), // direct conflicts; empty is legal (→ low_diversity flag)
|
|
249
|
+
unique: z.array(Claim), // raised by exactly one provider
|
|
250
|
+
blind_spots: z.array(z.string()), // rubric checklist items no provider addressed
|
|
251
|
+
});
|
|
252
|
+
// ── code-review: ReviewMap (§12.2, T10) ─────────────────────────────────────
|
|
253
|
+
//
|
|
254
|
+
// The code-review analog of DisagreementMap. Findings are line-anchored, so unlike idea's prose
|
|
255
|
+
// claims we CAN deterministically detect when both reviewers independently flagged the same bug
|
|
256
|
+
// (the §487 matcher: same file + overlapping lines + same category). Built pre-S9 from {reviewer
|
|
257
|
+
// findings, mutual cross-exam}; the judge then adjudicates only `disputed`. Final HIGH/MED/LOW
|
|
258
|
+
// confidence + false-positive exclusion are DERIVED at S10 (not stored here) — one source of truth.
|
|
259
|
+
// Internal (engine-built) → not strict.
|
|
260
|
+
/** The other reviewer's cross-exam verdict on a finding. `NONE` = both reviewers raised it
|
|
261
|
+
* independently (§487-matched), so no cross-exam was needed to confirm it. */
|
|
262
|
+
export const CrossVerdict = z.enum(['CONFIRM', 'REFUTE', 'UNCERTAIN', 'NONE']);
|
|
263
|
+
/** A finding tagged with who raised it, the other reviewer's cross-exam verdict, and (if disputed)
|
|
264
|
+
* the refuting argument. `reviewers` has 2 entries only when both independently found the same bug. */
|
|
265
|
+
export const AnnotatedFinding = z.object({
|
|
266
|
+
finding: Finding,
|
|
267
|
+
reviewers: z.array(ProviderIdSchema).min(1),
|
|
268
|
+
cross_verdict: CrossVerdict,
|
|
269
|
+
refutation: z.string().optional(),
|
|
270
|
+
});
|
|
271
|
+
export const ReviewMap = z.object({
|
|
272
|
+
consensus: z.array(AnnotatedFinding), // both-independent or CONFIRMed → HIGH
|
|
273
|
+
disputed: z.array(AnnotatedFinding), // REFUTEd → adjudicated by S9
|
|
274
|
+
single_reviewer: z.array(AnnotatedFinding), // one reviewer, UNCERTAIN/unexamined → MEDIUM
|
|
275
|
+
per_reviewer: z.array(z.object({ provider: ProviderIdSchema, raised: z.number().int().nonnegative(), kept: z.number().int().nonnegative(), dropped: z.number().int().nonnegative() })),
|
|
276
|
+
});
|
|
277
|
+
// ── S9: JudgeReport (§13) ───────────────────────────────────────────────────
|
|
278
|
+
const Adjudication = z
|
|
279
|
+
.object({
|
|
280
|
+
id: z.string().min(1), // disputed item id
|
|
281
|
+
ruling: z.enum(['UPHOLD', 'REJECT', 'UNRESOLVED']),
|
|
282
|
+
reasoning: z.string().min(1), // ≤3 sentences
|
|
283
|
+
evidence_cited: z.string().min(1),
|
|
284
|
+
})
|
|
285
|
+
.strict();
|
|
286
|
+
export const Recommendation = z.enum(['PROCEED', 'PROCEED_WITH_CONDITIONS', 'PIVOT', 'STOP']);
|
|
287
|
+
const JudgeReportBase = z
|
|
288
|
+
.object({
|
|
289
|
+
adjudications: z.array(Adjudication),
|
|
290
|
+
verdict: z.string().min(1), // the recommendation + core reason (idea: 2-5 sentences; grounded in adjudicated + consensus claims)
|
|
291
|
+
recommendation: Recommendation.optional(), // idea workflow; code-review omits it
|
|
292
|
+
conditions: z.array(z.string().min(1)).max(6).optional(), // present only for PROCEED_WITH_CONDITIONS
|
|
293
|
+
key_points: z.array(z.string()).max(10).optional(), // chairman's bulleted reasoning (idea workflow); code-review omits it
|
|
294
|
+
dissent: z.array(z.string()).min(1), // ≥1 — empty dissent is invalid (§9); strongest counter-argument
|
|
295
|
+
confidence_notes: z.string().min(1), // which conclusions are HIGH/MEDIUM/LOW and why
|
|
296
|
+
})
|
|
297
|
+
.strict();
|
|
298
|
+
export const JudgeReport = JudgeReportBase.superRefine((r, ctx) => {
|
|
299
|
+
const hasConditions = (r.conditions?.length ?? 0) > 0;
|
|
300
|
+
if (r.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
|
|
301
|
+
ctx.addIssue({
|
|
302
|
+
code: z.ZodIssueCode.custom,
|
|
303
|
+
path: ['conditions'],
|
|
304
|
+
message: 'conditions are required when recommendation is PROCEED_WITH_CONDITIONS',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (hasConditions && r.recommendation !== 'PROCEED_WITH_CONDITIONS') {
|
|
308
|
+
ctx.addIssue({
|
|
309
|
+
code: z.ZodIssueCode.custom,
|
|
310
|
+
path: ['conditions'],
|
|
311
|
+
message: 'conditions are only valid for PROCEED_WITH_CONDITIONS',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
/** S9 call-time variant: `dissent` relaxed to min-0 so an empty dissent does NOT auto-throw inside
|
|
316
|
+
* jsonCall. S9 enforces the non-empty rule itself (one re-ask → else flag `synthesis_suspect` +
|
|
317
|
+
* inject a placeholder) so it can salvage the rest of the report instead of failing the run (§260).
|
|
318
|
+
* Recommendation/condition consistency is also enforced inside idea S9, not by this relaxed schema. */
|
|
319
|
+
export const JudgeReportModel = JudgeReportBase.extend({
|
|
320
|
+
dissent: z.array(z.string()),
|
|
321
|
+
});
|
|
322
|
+
// ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
|
|
323
|
+
export const ActionPlan = z
|
|
324
|
+
.object({
|
|
325
|
+
actions: z
|
|
326
|
+
.array(z
|
|
327
|
+
.object({
|
|
328
|
+
order: z.number().int().min(1),
|
|
329
|
+
action: z.string().min(1),
|
|
330
|
+
why: z.string().min(1),
|
|
331
|
+
validates: z.string().min(1),
|
|
332
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
333
|
+
kill_signal: z.string().min(1),
|
|
334
|
+
})
|
|
335
|
+
.strict())
|
|
336
|
+
.min(1)
|
|
337
|
+
.max(7),
|
|
338
|
+
sequencing_note: z.string().min(1),
|
|
339
|
+
})
|
|
340
|
+
.strict();
|
|
341
|
+
// ── RunMeta (§15, §16) ──────────────────────────────────────────────────────
|
|
342
|
+
//
|
|
343
|
+
// Written by the artifact writer; assembled by the engine's RunCtx (T5). Internal → not strict.
|
|
344
|
+
/** One provider call's accounting entry (§15 "per-call timings"). */
|
|
345
|
+
export const CallRecord = z.object({
|
|
346
|
+
provider: ProviderIdSchema,
|
|
347
|
+
stage: z.string(), // e.g. "S4", "S1"
|
|
348
|
+
durationMs: z.number().nonnegative(),
|
|
349
|
+
error: z.enum(['NOT_FOUND', 'AUTH', 'QUOTA', 'TIMEOUT', 'BAD_OUTPUT', 'CRASH']).optional(),
|
|
350
|
+
});
|
|
351
|
+
/** How read-only was actually enforced per provider (§15, §19). Mirrors providers ReadOnlyFlag. */
|
|
352
|
+
const ReadOnlyFlagSchema = z.enum(['plan', 'sandbox', 'none']);
|
|
353
|
+
/** Resolved flag profile as recorded in meta (mirrors providers/types.ts FlagProfile). */
|
|
354
|
+
const FlagProfileSchema = z.object({
|
|
355
|
+
id: ProviderIdSchema,
|
|
356
|
+
jsonOutput: z.boolean(),
|
|
357
|
+
readOnlyFlag: ReadOnlyFlagSchema,
|
|
358
|
+
model: z.string().optional(),
|
|
359
|
+
});
|
|
360
|
+
export const RunMeta = z.object({
|
|
361
|
+
run_id: z.string().min(1), // encodes the timestamp (e.g. 20260702-1412-idea-refinement-a3f9)
|
|
362
|
+
workflow: WorkflowIdSchema,
|
|
363
|
+
provider_versions: z.record(ProviderIdSchema, z.string()), // detected `--version` strings
|
|
364
|
+
flag_profiles: z.record(ProviderIdSchema, FlagProfileSchema),
|
|
365
|
+
roles: z.record(z.string(), ProviderIdSchema), // role name → assigned provider
|
|
366
|
+
read_only: z.record(ProviderIdSchema, ReadOnlyFlagSchema), // enforcement level per provider
|
|
367
|
+
calls: z.array(CallRecord),
|
|
368
|
+
call_count: z.number().int().nonnegative(),
|
|
369
|
+
budget: z.object({ limit: z.number().int().positive(), used: z.number().int().nonnegative() }),
|
|
370
|
+
exit_status: z.enum(['ok', 'failed', 'aborted', 'partial']),
|
|
371
|
+
aborted: z.boolean(), // §16: Ctrl+C finalizes meta with aborted:true
|
|
372
|
+
// §16 report-header flags; absent = none.
|
|
373
|
+
flags: z.array(z.enum(['synthesis_suspect', 'low_diversity', 'plan_skipped', 'plan_fallback'])).optional(),
|
|
374
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Judge playbook — adjudicate disputed findings
|
|
2
|
+
|
|
3
|
+
You rule on findings two reviewers disagreed on, from the evidence and refutation text ALONE — you
|
|
4
|
+
cannot open the repo. Rule on what is proven, not on who sounds confident.
|
|
5
|
+
|
|
6
|
+
## How to weigh each dispute
|
|
7
|
+
- Prefer the side backed by concrete, checkable evidence (a specific line, input, or behavior) over
|
|
8
|
+
the side that only asserts. A refutation with no mechanism is weak.
|
|
9
|
+
- UPHOLD when the evidence names a real trigger and the refutation does not actually neutralize it.
|
|
10
|
+
- REJECT when the refutation shows the finding misreads the code, the trigger cannot occur, or the
|
|
11
|
+
path is already guarded.
|
|
12
|
+
- UNRESOLVED only when both sides are genuinely balanced and the text cannot settle it. Do not use
|
|
13
|
+
UNRESOLVED to avoid deciding — it is the rare case, not the safe default.
|
|
14
|
+
|
|
15
|
+
## Severity discipline
|
|
16
|
+
- An upheld P0/P1 (correctness / security / data-loss) needs a demonstrated failure, not a worry.
|
|
17
|
+
- If the defect is real but its severity is inflated, uphold it and say the severity is lower in your
|
|
18
|
+
reasoning — do not reject a real bug over its label.
|
|
19
|
+
|
|
20
|
+
## Verdict and dissent
|
|
21
|
+
- The verdict states roughly how many real defects survive and the worst severity — no hedging.
|
|
22
|
+
- Your dissent must be the strongest honest argument against your OWN verdict, not a throwaway. If you
|
|
23
|
+
cannot argue against yourself, you have not stress-tested the ruling.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Reviewer playbook — hunt for real defects
|
|
2
|
+
|
|
3
|
+
You are hunting for bugs a single reviewer would miss. Depth over breadth: a few well-evidenced
|
|
4
|
+
defects beat a long list of guesses.
|
|
5
|
+
|
|
6
|
+
## Hunt order (chase behavior, not style)
|
|
7
|
+
1. CORRECTNESS — off-by-one, inverted condition, wrong operator/variable, missing return,
|
|
8
|
+
unhandled null/undefined, broken control flow, wrong default.
|
|
9
|
+
2. SECURITY — unvalidated input, injection (SQL/shell/path), authz/authn gaps, secret or PII
|
|
10
|
+
exposure, unsafe deserialization, missing access checks.
|
|
11
|
+
3. CONCURRENCY — races, unawaited promises, shared mutable state, lost updates, ordering
|
|
12
|
+
assumptions, deadlock.
|
|
13
|
+
4. ERROR_HANDLING — swallowed errors, wrong error type, partial failure leaving bad state,
|
|
14
|
+
missing cleanup on the failure path.
|
|
15
|
+
5. PERF — accidental O(n^2), work inside a loop that belongs outside, N+1 calls, unbounded
|
|
16
|
+
growth. Only when the impact is real, not theoretical.
|
|
17
|
+
|
|
18
|
+
## Trace the change, don't just read it
|
|
19
|
+
- For each changed function: who calls it, with what values, and what happens on the failure
|
|
20
|
+
path? Read the surrounding code before judging.
|
|
21
|
+
- Ask "what input breaks this?" Name that concrete input in your evidence.
|
|
22
|
+
- Check the boundaries: empty, zero, negative, very large, null, and concurrent access.
|
|
23
|
+
|
|
24
|
+
## Evidence bar (a finding without this is noise)
|
|
25
|
+
- `evidence` must quote the exact code or describe the exact behavior that proves the defect —
|
|
26
|
+
not a restatement of the claim.
|
|
27
|
+
- Give the concrete trigger, e.g. "when `items` is empty, line 42 divides by zero".
|
|
28
|
+
- If you cannot point to the line that fails, do not report it.
|
|
29
|
+
|
|
30
|
+
## Confidence, honestly
|
|
31
|
+
- 0.9+ : you can name the input and the wrong result it produces.
|
|
32
|
+
- 0.6-0.8 : likely defect, but rests on an assumption about callers or runtime.
|
|
33
|
+
- <0.5 : a smell worth flagging, not a claim — say so in the `claim`.
|
|
34
|
+
|
|
35
|
+
## Do not
|
|
36
|
+
- No style, naming, or formatting nits (nothing below P2).
|
|
37
|
+
- No speculative "could be a problem" without a concrete trigger.
|
|
38
|
+
- No findings on files or lines outside the diff.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Analyst playbook — pressure-test the idea
|
|
2
|
+
|
|
3
|
+
You are an adversarial analyst on a decision panel. Your job is not to like the idea — it is to find
|
|
4
|
+
where it breaks. Steelman it once, then attack it honestly.
|
|
5
|
+
|
|
6
|
+
## Strongest version first
|
|
7
|
+
- Before attacking, state the best honest version of the idea: who it is for and why it would win.
|
|
8
|
+
- Attack THAT version, not a weak strawman. If the steelman is thin, that itself is a finding.
|
|
9
|
+
|
|
10
|
+
## Assumptions — surface the load-bearing ones
|
|
11
|
+
- Name the assumptions the idea silently depends on. Mark each VERIFIABLE (checkable with data or a
|
|
12
|
+
test) or JUDGMENT (a bet on taste, market, or behavior).
|
|
13
|
+
- Flag which are load-bearing: the ones where, if false, the whole idea collapses. Spend effort
|
|
14
|
+
there, not on cosmetic assumptions.
|
|
15
|
+
|
|
16
|
+
## Attacks — anchored and severe
|
|
17
|
+
- Every attack must target a specific assumption id. An attack that floats free of an assumption is
|
|
18
|
+
noise — discard it yourself.
|
|
19
|
+
- Rank by severity: HIGH = kills or reshapes the idea; MED = forces a costly change; LOW = a caveat.
|
|
20
|
+
- Prefer the attack a smart skeptic would actually make: no strawmen, no nitpicks dressed as risks.
|
|
21
|
+
|
|
22
|
+
## MANDATORY coverage — address every dimension below
|
|
23
|
+
Do not leave a dimension unexamined; an unaddressed dimension is a hole in your analysis, not a
|
|
24
|
+
neutral omission. For each, either raise an assumption/attack or state plainly why it is not a
|
|
25
|
+
concern for THIS idea:
|
|
26
|
+
1. Target user / who this is actually for.
|
|
27
|
+
2. Existing alternatives — including "do nothing" and the status quo.
|
|
28
|
+
3. Differentiation — why this wins over those alternatives.
|
|
29
|
+
4. Feasibility — can it actually be built, under real constraints.
|
|
30
|
+
5. Cost / effort / resources to build and run it.
|
|
31
|
+
6. Policy / legal / compliance / privacy risk.
|
|
32
|
+
7. Kill criteria — what evidence would make you abandon it.
|
|
33
|
+
8. Business model / how it makes money (or why it needn't).
|
|
34
|
+
9. Distribution / go-to-market — how anyone even finds it.
|
|
35
|
+
10. Timing — why now, and what makes the market ready or not.
|
|
36
|
+
11. Scalability — what breaks as it grows.
|
|
37
|
+
12. Key risks / assumptions that must be validated first.
|
|
38
|
+
|
|
39
|
+
## Open questions that move the verdict
|
|
40
|
+
- List only questions whose ANSWER would change the decision. "Nice to know" is not one of them.
|
|
41
|
+
- Phrase each so that a yes/no or a single number would actually flip your assessment.
|
|
42
|
+
|
|
43
|
+
## Do not
|
|
44
|
+
- No motivational framing, no pitch language, no summarizing your own output.
|
|
45
|
+
- No attack without an assumption to anchor it. No assumption you cannot tie to the idea.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Planner playbook — decisive validation
|
|
2
|
+
|
|
3
|
+
You write the next tests after the council has already judged the idea. Do not plan the build.
|
|
4
|
+
Plan only the cheapest validation work that can change the decision.
|
|
5
|
+
|
|
6
|
+
## Order
|
|
7
|
+
- Start with upheld HIGH risks. If one fails, the idea should stop or change shape before any other work.
|
|
8
|
+
- Then cover blind spots that could hide a blocker.
|
|
9
|
+
- Then answer open questions that would flip the verdict.
|
|
10
|
+
|
|
11
|
+
## Action quality
|
|
12
|
+
- Every action must be concrete enough that a person can run it this week.
|
|
13
|
+
- Prefer interviews, landing-page smoke tests, pricing tests, manual concierge trials, prototype probes,
|
|
14
|
+
competitor checks, and policy checks over implementation work.
|
|
15
|
+
- Tie every action to exactly one anchor in `validates`.
|
|
16
|
+
|
|
17
|
+
## Kill signals
|
|
18
|
+
- A kill signal is not "learn more". It is the result that stops, pivots, or narrows the idea.
|
|
19
|
+
- Make the signal observable: a count, threshold, refusal pattern, legal blocker, cost ceiling, or
|
|
20
|
+
behavior that contradicts the assumption.
|
|
21
|
+
|
|
22
|
+
## Do not
|
|
23
|
+
- No product roadmap, scaffolding plan, architecture plan, or feature list.
|
|
24
|
+
- No action for a claim the chair already settled.
|
|
25
|
+
- No vague market research. No unanchored action.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// `.aiki/feedback.jsonl` — the human review gate (`aiki resolve`, §127/§444/§618). Append-only.
|
|
2
|
+
// This is the PURE core (schema + build + append); the interactive readline shell lives in cli/resolve.ts.
|
|
3
|
+
// Keeping the writes pure is what lets §604/§606 ("resolve appends valid JSONL") be tested without a TTY.
|
|
4
|
+
//
|
|
5
|
+
// Workflow-aware (T11): idea-refinement annotates the judge's adjudicated contradictions with
|
|
6
|
+
// correct/incorrect/unsure; code-review annotates the report's kept findings with fixed/wontfix/
|
|
7
|
+
// false-positive (the false-positive labels feed the bench PRECISION metric, BENCHMARK.md §2). Each line
|
|
8
|
+
// snapshots the item's `ruling` (a free string: idea = UPHOLD/REJECT/UNRESOLVED; code-review = severity/
|
|
9
|
+
// category/confidence) so it's self-describing for the bench scorer without rejoining to the run.
|
|
10
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { WorkflowIdSchema } from '../schemas/index.js';
|
|
14
|
+
/** All human verdicts across workflows. `parseVerdictFlags`/`resolve` restrict to the per-workflow vocab. */
|
|
15
|
+
export const VerdictSchema = z.enum(['correct', 'incorrect', 'unsure', 'fixed', 'wontfix', 'false-positive']);
|
|
16
|
+
export const VERDICT_VOCAB = {
|
|
17
|
+
'idea-refinement': ['correct', 'incorrect', 'unsure'],
|
|
18
|
+
'code-review': ['fixed', 'wontfix', 'false-positive'],
|
|
19
|
+
};
|
|
20
|
+
/** One appended feedback line. `item_type` distinguishes an idea adjudication from a code-review finding. */
|
|
21
|
+
export const FeedbackEntry = z
|
|
22
|
+
.object({
|
|
23
|
+
run_id: z.string().min(1),
|
|
24
|
+
workflow: WorkflowIdSchema,
|
|
25
|
+
item_type: z.enum(['adjudication', 'finding']),
|
|
26
|
+
item_id: z.string().min(1),
|
|
27
|
+
verdict: VerdictSchema,
|
|
28
|
+
ruling: z.string().min(1), // snapshot of what the human reacted to (workflow-specific)
|
|
29
|
+
at: z.string().min(1), // ISO-8601
|
|
30
|
+
note: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
export class FeedbackError extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'FeedbackError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build validated feedback entries from a verdict map, in `items` order. A verdict referencing an
|
|
41
|
+
* unknown item id is a hard error (typo guard — mirrors the config hard-fail rule).
|
|
42
|
+
*/
|
|
43
|
+
export function buildFeedbackEntries(runId, workflow, items, verdicts, at = new Date(), itemType = 'adjudication') {
|
|
44
|
+
const known = new Set(items.map((i) => i.id));
|
|
45
|
+
for (const id of verdicts.keys()) {
|
|
46
|
+
if (!known.has(id))
|
|
47
|
+
throw new FeedbackError(`no ${itemType} "${id}" in this run (have: ${[...known].join(', ') || 'none'})`);
|
|
48
|
+
}
|
|
49
|
+
const iso = at.toISOString();
|
|
50
|
+
const entries = [];
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
const v = verdicts.get(item.id);
|
|
53
|
+
if (!v)
|
|
54
|
+
continue;
|
|
55
|
+
entries.push(FeedbackEntry.parse({
|
|
56
|
+
run_id: runId,
|
|
57
|
+
workflow,
|
|
58
|
+
item_type: itemType,
|
|
59
|
+
item_id: item.id,
|
|
60
|
+
verdict: v.verdict,
|
|
61
|
+
ruling: item.ruling,
|
|
62
|
+
at: iso,
|
|
63
|
+
...(v.note ? { note: v.note } : {}),
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
return entries;
|
|
67
|
+
}
|
|
68
|
+
const VERDICT_ALIAS = {
|
|
69
|
+
c: 'correct',
|
|
70
|
+
correct: 'correct',
|
|
71
|
+
i: 'incorrect',
|
|
72
|
+
incorrect: 'incorrect',
|
|
73
|
+
u: 'unsure',
|
|
74
|
+
unsure: 'unsure',
|
|
75
|
+
f: 'fixed',
|
|
76
|
+
fixed: 'fixed',
|
|
77
|
+
w: 'wontfix',
|
|
78
|
+
wontfix: 'wontfix',
|
|
79
|
+
fp: 'false-positive',
|
|
80
|
+
'false-positive': 'false-positive',
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Parse repeatable `--verdict <id>=<verdict>` flags → a verdict map. `allowed` restricts to a workflow's
|
|
84
|
+
* vocab (default: all). Malformed flag or out-of-vocab verdict throws FeedbackError (hard-fail).
|
|
85
|
+
*/
|
|
86
|
+
export function parseVerdictFlags(flags, allowed = VerdictSchema.options) {
|
|
87
|
+
const ok = new Set(allowed);
|
|
88
|
+
const m = new Map();
|
|
89
|
+
for (const f of flags) {
|
|
90
|
+
const eq = f.indexOf('=');
|
|
91
|
+
if (eq < 1)
|
|
92
|
+
throw new FeedbackError(`bad --verdict "${f}" — use <id>=<${allowed.join('|')}>`);
|
|
93
|
+
const id = f.slice(0, eq).trim();
|
|
94
|
+
const verdict = VERDICT_ALIAS[f.slice(eq + 1).trim().toLowerCase()];
|
|
95
|
+
if (!id)
|
|
96
|
+
throw new FeedbackError(`bad --verdict "${f}" — missing item id`);
|
|
97
|
+
if (!verdict || !ok.has(verdict))
|
|
98
|
+
throw new FeedbackError(`bad verdict for "${id}" — use ${allowed.join('|')}`);
|
|
99
|
+
m.set(id, { verdict });
|
|
100
|
+
}
|
|
101
|
+
return m;
|
|
102
|
+
}
|
|
103
|
+
/** Append entries to `<root>/feedback.jsonl` (one JSON object per line). No-op on empty input. */
|
|
104
|
+
export async function appendFeedback(entries, root = '.aiki') {
|
|
105
|
+
const path = join(root, 'feedback.jsonl');
|
|
106
|
+
if (entries.length === 0)
|
|
107
|
+
return path;
|
|
108
|
+
await mkdir(root, { recursive: true });
|
|
109
|
+
await appendFile(path, entries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf8');
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Where aiki keeps its files when launched from anywhere (hybrid model, user decision 2026-07-06).
|
|
2
|
+
//
|
|
3
|
+
// runs → the project's `.aiki/` when you are inside a git repo, else the global `~/.aiki/`.
|
|
4
|
+
// So project work stays co-located with its project, but running from home still has a home.
|
|
5
|
+
// home → `~/.aiki/` holds the global session registry (+ later: global config).
|
|
6
|
+
//
|
|
7
|
+
// Library defaults elsewhere stay `.aiki` (cwd-relative) for tests/back-compat; only the CLI entry
|
|
8
|
+
// points resolve the hybrid root and inject it. Kept tiny + pure-ish (one git probe) on purpose.
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { repoToplevel } from '../orchestration/git.js';
|
|
12
|
+
/** Global aiki home — `$AIKI_HOME` if set, else `~/.aiki`. Holds the cross-location session registry. */
|
|
13
|
+
export function homeAikiRoot() {
|
|
14
|
+
return process.env.AIKI_HOME ?? join(homedir(), '.aiki');
|
|
15
|
+
}
|
|
16
|
+
/** Hybrid runs root for `cwd`: `<repoRoot>/.aiki` inside a git repo, else `~/.aiki`. */
|
|
17
|
+
export async function resolveRunsRoot(cwd = process.cwd()) {
|
|
18
|
+
const repo = await repoToplevel(cwd);
|
|
19
|
+
return repo ? join(repo, '.aiki') : homeAikiRoot();
|
|
20
|
+
}
|
|
Binary file
|