vibe-coding-master 0.7.28 → 0.7.29
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/README.md +26 -8
- package/dist/backend/api/harness-routes.js +0 -1
- package/dist/backend/server.js +9 -6
- package/dist/backend/services/architect-restart-service.js +51 -3
- package/dist/backend/services/auto-memory-service.js +118 -61
- package/dist/backend/services/claude-hook-service.js +12 -2
- package/dist/backend/services/harness-feedback-service.js +138 -7
- package/dist/backend/services/memory-proposal-validation.js +80 -0
- package/dist/backend/services/memory-review-paths.js +7 -0
- package/dist/backend/services/memory-review-validation.js +94 -0
- package/dist/backend/templates/harness/architect-agent.js +2 -1
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
- package/dist/backend/templates/harness/restart-architect-skill.js +12 -2
- package/dist/backend/templates/harness/role-memory.js +2 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -451,7 +451,7 @@ Use it to:
|
|
|
451
451
|
- view and edit shared or role-specific VCM memory
|
|
452
452
|
- review and revert memory changes recorded in the active task worktree
|
|
453
453
|
- copy file paths for discussion
|
|
454
|
-
- review task harness
|
|
454
|
+
- review task harness and consolidate optional Auto Memory in one retrospective
|
|
455
455
|
- inspect commit diffs for harness changes
|
|
456
456
|
- merge task harness commits back to the connected repository branch when
|
|
457
457
|
appropriate
|
|
@@ -467,7 +467,14 @@ files.
|
|
|
467
467
|
Review Task Harness after Final Acceptance, Project Manager, Architect, Coder,
|
|
468
468
|
Tester, and an enabled Reviewer submit proposals in sequence through
|
|
469
469
|
`vcm-propose-memory`. Harness Engineer verifies and consolidates them before VCM
|
|
470
|
-
applies the result
|
|
470
|
+
applies the result as part of the same Task Harness Retrospective. Roles cannot
|
|
471
|
+
edit active memory directly.
|
|
472
|
+
|
|
473
|
+
When Auto Memory is enabled, the planning Architect writes a provisional memory
|
|
474
|
+
candidate before its post-planning Session restart. VCM snapshots that candidate
|
|
475
|
+
into the later memory-review run. The replacement Architect validates it against
|
|
476
|
+
the completed implementation and tests, and Harness Engineer reviews it with all
|
|
477
|
+
final role proposals before anything becomes active memory.
|
|
471
478
|
|
|
472
479
|
Shared memory is stored in the root `CLAUDE.md` `<VCM-memory>` block. Role memory
|
|
473
480
|
is stored in the matching `.claude/agents/*.md` block. VCM changes only block
|
|
@@ -481,21 +488,32 @@ Post-task processing is ordered by the backend:
|
|
|
481
488
|
```text
|
|
482
489
|
Final Acceptance
|
|
483
490
|
-> Review Task Harness
|
|
491
|
+
-> Snapshot Architect planning-session memory candidate, when present
|
|
484
492
|
-> Workflow-role memory proposals, when Auto Memory is enabled
|
|
485
|
-
-> Harness Engineer memory review, when Auto Memory is enabled
|
|
486
493
|
-> Task Harness Retrospective
|
|
494
|
+
-> Review pending Harness Feedback
|
|
495
|
+
-> Review Auto Memory proposals, when Auto Memory is enabled
|
|
496
|
+
-> Apply reviewed memory, when Auto Memory is enabled
|
|
487
497
|
```
|
|
488
498
|
|
|
489
499
|
Memory proposal prompts sent to Project Manager, Architect, Coder, Tester, and
|
|
490
500
|
an enabled Reviewer use their normal task sessions and participate in
|
|
491
|
-
Round/Turn tracking.
|
|
492
|
-
role
|
|
501
|
+
Round/Turn tracking. Each proposed add, update, or removal identifies its
|
|
502
|
+
shared or current-role target and its supporting evidence. Adds and updates
|
|
503
|
+
also state why the memory is necessary, the impact of omitting it, and whether
|
|
504
|
+
the knowledge belongs in memory or a durable document. Harness Engineer first
|
|
505
|
+
reviews every existing memory entry, recording its retention reason, removal
|
|
506
|
+
impact, and durable-document disposition, then evaluates the proposals and
|
|
507
|
+
records the resulting changes in the Retrospective report. VCM validates that
|
|
508
|
+
report structure before applying the reviewed memory. Harness Engineer review
|
|
509
|
+
and retrospective work remain tool role activity and do not participate in
|
|
510
|
+
Round completion.
|
|
493
511
|
|
|
494
512
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
495
513
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
|
496
|
-
review requests
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
review requests collect proposals before starting the retrospective. A failed
|
|
515
|
+
proposal collection or combined retrospective memory review must be retried
|
|
516
|
+
from Harness Studio.
|
|
499
517
|
|
|
500
518
|
## Closing a Task
|
|
501
519
|
|
|
@@ -174,7 +174,6 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
174
174
|
if (!memoryReadiness.ready) {
|
|
175
175
|
return deps.harnessFeedbackService.getState(project.repoRoot, task.taskSlug);
|
|
176
176
|
}
|
|
177
|
-
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
178
177
|
return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
|
|
179
178
|
taskSlug: task.taskSlug,
|
|
180
179
|
taskRepoRoot: task.worktreePath,
|
package/dist/backend/server.js
CHANGED
|
@@ -251,11 +251,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
251
251
|
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
|
|
252
252
|
vcmVersion
|
|
253
253
|
});
|
|
254
|
-
const harnessFeedbackService = createHarnessFeedbackService({
|
|
255
|
-
fs,
|
|
256
|
-
runtime,
|
|
257
|
-
sessionService
|
|
258
|
-
});
|
|
259
254
|
const autoMemoryService = createAutoMemoryService({
|
|
260
255
|
fs,
|
|
261
256
|
git,
|
|
@@ -266,6 +261,12 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
266
261
|
return true;
|
|
267
262
|
}
|
|
268
263
|
});
|
|
264
|
+
const harnessFeedbackService = createHarnessFeedbackService({
|
|
265
|
+
fs,
|
|
266
|
+
runtime,
|
|
267
|
+
sessionService,
|
|
268
|
+
autoMemoryService
|
|
269
|
+
});
|
|
269
270
|
const commandDispatcher = createCommandDispatcher({
|
|
270
271
|
runtime,
|
|
271
272
|
sessionService,
|
|
@@ -280,7 +281,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
280
281
|
const architectRestartService = createArchitectRestartService({
|
|
281
282
|
fs,
|
|
282
283
|
taskService,
|
|
283
|
-
sessionService
|
|
284
|
+
sessionService,
|
|
285
|
+
appSettings
|
|
284
286
|
});
|
|
285
287
|
const messageService = createMessageService({
|
|
286
288
|
fs,
|
|
@@ -388,6 +390,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
388
390
|
runtime,
|
|
389
391
|
harnessService,
|
|
390
392
|
autoMemoryService,
|
|
393
|
+
harnessFeedbackService,
|
|
391
394
|
gatewayService,
|
|
392
395
|
jobGuard: createJobGuardService(),
|
|
393
396
|
translationWorkerService,
|
|
@@ -2,6 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
5
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH } from "./memory-review-paths.js";
|
|
6
|
+
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
5
7
|
const ARCHITECT_ROLE = "architect";
|
|
6
8
|
const PM_ROLE = "project-manager";
|
|
7
9
|
const COMPLETE_PLAN_PATTERN = /^Planning Result:\s*complete\s*$/im;
|
|
@@ -21,6 +23,17 @@ export function createArchitectRestartService(deps) {
|
|
|
21
23
|
async schedule(repoRoot, taskSlug) {
|
|
22
24
|
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
23
25
|
await requireCompletePlan(repoRoot, taskSlug);
|
|
26
|
+
const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
|
|
27
|
+
? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
|
|
28
|
+
: undefined;
|
|
29
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
30
|
+
const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH);
|
|
31
|
+
if (deps.fs.removePath) {
|
|
32
|
+
await deps.fs.removePath(candidatePath, { force: true });
|
|
33
|
+
}
|
|
34
|
+
else if (await deps.fs.pathExists(candidatePath)) {
|
|
35
|
+
await deps.fs.writeText(candidatePath, "");
|
|
36
|
+
}
|
|
24
37
|
const key = taskKey(repoRoot, taskSlug);
|
|
25
38
|
const existing = pendingByTask.get(key);
|
|
26
39
|
if (existing?.sessionId === session.id) {
|
|
@@ -29,7 +42,13 @@ export function createArchitectRestartService(deps) {
|
|
|
29
42
|
existing.acceptedMessageId = undefined;
|
|
30
43
|
existing.gateAccepted = false;
|
|
31
44
|
existing.executing = false;
|
|
32
|
-
|
|
45
|
+
existing.memoryCandidatePath = memoryCandidatePath;
|
|
46
|
+
return {
|
|
47
|
+
taskSlug,
|
|
48
|
+
sessionId: session.id,
|
|
49
|
+
status: "scheduled",
|
|
50
|
+
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
51
|
+
};
|
|
33
52
|
}
|
|
34
53
|
pendingByTask.set(key, {
|
|
35
54
|
repoRoot,
|
|
@@ -37,9 +56,15 @@ export function createArchitectRestartService(deps) {
|
|
|
37
56
|
sessionId: session.id,
|
|
38
57
|
stopped: false,
|
|
39
58
|
gateAccepted: false,
|
|
40
|
-
executing: false
|
|
59
|
+
executing: false,
|
|
60
|
+
memoryCandidatePath
|
|
41
61
|
});
|
|
42
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
taskSlug,
|
|
64
|
+
sessionId: session.id,
|
|
65
|
+
status: "scheduled",
|
|
66
|
+
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
67
|
+
};
|
|
43
68
|
},
|
|
44
69
|
async recordArchitectStop(repoRoot, taskSlug, sessionId) {
|
|
45
70
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
@@ -123,6 +148,7 @@ export function createArchitectRestartService(deps) {
|
|
|
123
148
|
pending.executing = true;
|
|
124
149
|
try {
|
|
125
150
|
await requireCompletePlan(pending.repoRoot, pending.taskSlug);
|
|
151
|
+
await requirePlanningMemoryCandidate(pending);
|
|
126
152
|
await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
127
153
|
permissionMode: session.permissionMode,
|
|
128
154
|
model: session.model,
|
|
@@ -135,6 +161,21 @@ export function createArchitectRestartService(deps) {
|
|
|
135
161
|
pending.executing = false;
|
|
136
162
|
}
|
|
137
163
|
}
|
|
164
|
+
async function requirePlanningMemoryCandidate(pending) {
|
|
165
|
+
if (!pending.memoryCandidatePath) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const task = await deps.taskService.loadTask(pending.repoRoot, pending.taskSlug);
|
|
169
|
+
const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), pending.memoryCandidatePath);
|
|
170
|
+
if (!(await deps.fs.pathExists(candidatePath))) {
|
|
171
|
+
throw invalidMemoryCandidateError("the assigned candidate file does not exist.");
|
|
172
|
+
}
|
|
173
|
+
const content = await deps.fs.readText(candidatePath);
|
|
174
|
+
const validationError = validateMemoryProposal(content);
|
|
175
|
+
if (validationError) {
|
|
176
|
+
throw invalidMemoryCandidateError(`the assigned candidate ${validationError}.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
138
179
|
}
|
|
139
180
|
function isArchitectToPm(message) {
|
|
140
181
|
return message.fromRole === ARCHITECT_ROLE && message.toRole === PM_ROLE;
|
|
@@ -149,3 +190,10 @@ function incompletePlanError(reason) {
|
|
|
149
190
|
statusCode: 409
|
|
150
191
|
});
|
|
151
192
|
}
|
|
193
|
+
function invalidMemoryCandidateError(reason) {
|
|
194
|
+
return new VcmError({
|
|
195
|
+
code: "ARCHITECT_MEMORY_CANDIDATE_INVALID",
|
|
196
|
+
message: `Architect restart is waiting for its planning-session memory candidate because ${reason}`,
|
|
197
|
+
statusCode: 409
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -5,9 +5,9 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
|
+
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
+
import { validateMemoryReviewReport } from "./memory-review-validation.js";
|
|
11
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
12
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
13
|
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
@@ -132,7 +132,6 @@ export function createAutoMemoryService(deps) {
|
|
|
132
132
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
133
133
|
}
|
|
134
134
|
if (active?.status === "reviewing") {
|
|
135
|
-
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
136
135
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
137
136
|
}
|
|
138
137
|
if (active || !input.roundReady || !input.requestTrigger) {
|
|
@@ -174,6 +173,7 @@ export function createAutoMemoryService(deps) {
|
|
|
174
173
|
const before = await readMemorySet(input.taskRepoRoot);
|
|
175
174
|
await writeRunMemorySet(input.taskRepoRoot, runId, "before", before);
|
|
176
175
|
await writeRunMemorySet(input.taskRepoRoot, runId, "after", before);
|
|
176
|
+
await snapshotArchitectPlanningCandidate(input.taskRepoRoot, runId);
|
|
177
177
|
await persistRun(input.taskRepoRoot, {
|
|
178
178
|
version: 1,
|
|
179
179
|
runId,
|
|
@@ -202,6 +202,13 @@ export function createAutoMemoryService(deps) {
|
|
|
202
202
|
const active = await loadActiveState(input.taskRepoRoot);
|
|
203
203
|
if (active) {
|
|
204
204
|
const disposition = active.status;
|
|
205
|
+
if (disposition === "reviewing") {
|
|
206
|
+
return {
|
|
207
|
+
ready: true,
|
|
208
|
+
disposition,
|
|
209
|
+
trigger: active.trigger
|
|
210
|
+
};
|
|
211
|
+
}
|
|
205
212
|
return {
|
|
206
213
|
ready: false,
|
|
207
214
|
disposition,
|
|
@@ -218,9 +225,60 @@ export function createAutoMemoryService(deps) {
|
|
|
218
225
|
return {
|
|
219
226
|
ready: false,
|
|
220
227
|
disposition: "pending",
|
|
221
|
-
reason: "Auto Memory must
|
|
228
|
+
reason: "Auto Memory proposals must be collected before Task Harness Retrospective."
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function prepareTaskRetrospectiveReview(taskRepoRoot, retrospectiveReportPath) {
|
|
232
|
+
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
236
|
+
if (!state) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
if (state.status !== "reviewing") {
|
|
240
|
+
throw new VcmError({
|
|
241
|
+
code: "AUTO_MEMORY_NOT_READY",
|
|
242
|
+
message: "Auto Memory proposals are not ready for Task Harness Retrospective.",
|
|
243
|
+
statusCode: 409,
|
|
244
|
+
hint: "Wait for every workflow role to finish its memory proposal, then retry Task Harness Retrospective."
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (state.reviewPromptDispatchedAt) {
|
|
248
|
+
throw new VcmError({
|
|
249
|
+
code: "AUTO_MEMORY_REVIEW_RUNNING",
|
|
250
|
+
message: "Task Harness Retrospective is already reviewing Auto Memory.",
|
|
251
|
+
statusCode: 409
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const timestamp = now();
|
|
255
|
+
state.reviewPromptDispatchedAt = timestamp;
|
|
256
|
+
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
257
|
+
state.updatedAt = timestamp;
|
|
258
|
+
await persistActiveState(taskRepoRoot, state);
|
|
259
|
+
const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
|
|
260
|
+
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
261
|
+
return {
|
|
262
|
+
runId: state.runId,
|
|
263
|
+
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
264
|
+
currentMemoryPath: path.join(runRoot, "before"),
|
|
265
|
+
reviewedMemoryPath: path.join(runRoot, "after"),
|
|
266
|
+
proposalRoles: state.drafts.map((draft) => draft.role),
|
|
267
|
+
...(planningCandidatePath
|
|
268
|
+
? { planningCandidatePath: resolveRepoPath(taskRepoRoot, planningCandidatePath) }
|
|
269
|
+
: {})
|
|
222
270
|
};
|
|
223
271
|
}
|
|
272
|
+
async function cancelTaskRetrospectiveReview(taskRepoRoot, runId) {
|
|
273
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
274
|
+
if (!state || state.runId !== runId || state.status !== "reviewing") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
delete state.reviewPromptDispatchedAt;
|
|
278
|
+
delete state.retrospectiveReportPath;
|
|
279
|
+
state.updatedAt = now();
|
|
280
|
+
await persistActiveState(taskRepoRoot, state);
|
|
281
|
+
}
|
|
224
282
|
async function isRoleMemoryTurn(taskRepoRoot, role) {
|
|
225
283
|
const state = await loadActiveState(taskRepoRoot);
|
|
226
284
|
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
@@ -255,8 +313,9 @@ export function createAutoMemoryService(deps) {
|
|
|
255
313
|
return true;
|
|
256
314
|
}
|
|
257
315
|
const content = (await deps.fs.readText(draftAbsolutePath)).trim();
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
const validationError = content ? validateMemoryProposal(content) : "is empty";
|
|
317
|
+
if (validationError) {
|
|
318
|
+
await failReview(input.taskRepoRoot, state, `${input.role} memory draft ${validationError}.`);
|
|
260
319
|
return true;
|
|
261
320
|
}
|
|
262
321
|
draft.status = "completed";
|
|
@@ -270,7 +329,6 @@ export function createAutoMemoryService(deps) {
|
|
|
270
329
|
state.status = "reviewing";
|
|
271
330
|
await persistActiveState(input.taskRepoRoot, state);
|
|
272
331
|
await updateRunStatus(input.taskRepoRoot, state.runId, "reviewing", state.updatedAt);
|
|
273
|
-
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
274
332
|
return true;
|
|
275
333
|
}
|
|
276
334
|
async function handleHarnessEngineerHook(input) {
|
|
@@ -292,10 +350,23 @@ export function createAutoMemoryService(deps) {
|
|
|
292
350
|
return true;
|
|
293
351
|
}
|
|
294
352
|
if (input.eventName === "StopFailure") {
|
|
295
|
-
await failReview(input.taskRepoRoot, state, "Harness
|
|
353
|
+
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective memory review turn failed.");
|
|
296
354
|
return true;
|
|
297
355
|
}
|
|
298
356
|
if (input.eventName === "Stop") {
|
|
357
|
+
if (!state.retrospectiveReportPath
|
|
358
|
+
|| !(await deps.fs.pathExists(state.retrospectiveReportPath))
|
|
359
|
+
|| !(await deps.fs.readText(state.retrospectiveReportPath)).trim()) {
|
|
360
|
+
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective did not write the required retrospective report.");
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
const report = await deps.fs.readText(state.retrospectiveReportPath);
|
|
364
|
+
const currentMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "before");
|
|
365
|
+
const reportError = validateMemoryReviewReport(report, state.drafts.map((draft) => draft.role), hasSubstantiveMemory(currentMemorySnapshot));
|
|
366
|
+
if (reportError) {
|
|
367
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review report ${reportError}.`);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
299
370
|
await applyReviewedMemory(input.taskRepoRoot, state);
|
|
300
371
|
return true;
|
|
301
372
|
}
|
|
@@ -419,42 +490,15 @@ export function createAutoMemoryService(deps) {
|
|
|
419
490
|
draft.status = "dispatched";
|
|
420
491
|
state.updatedAt = now();
|
|
421
492
|
await persistActiveState(taskRepoRoot, state);
|
|
422
|
-
|
|
493
|
+
const planningCandidatePath = draft.role === "architect"
|
|
494
|
+
? await findPlanningCandidateSnapshot(taskRepoRoot, state.runId)
|
|
495
|
+
: undefined;
|
|
496
|
+
await submitTerminalInput(deps.runtime, session.id, buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath));
|
|
423
497
|
}
|
|
424
498
|
catch (error) {
|
|
425
499
|
await failReview(taskRepoRoot, state, `Unable to start ${draft.role} memory draft: ${errorMessage(error)}`);
|
|
426
500
|
}
|
|
427
501
|
}
|
|
428
|
-
async function dispatchHarnessReview(baseRepoRoot, taskRepoRoot, state) {
|
|
429
|
-
if (state.status !== "reviewing" || state.reviewPromptDispatchedAt) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (deps.isHarnessEngineerAvailable && !(await deps.isHarnessEngineerAvailable(baseRepoRoot))) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer");
|
|
437
|
-
if (existing?.activityStatus === "running") {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const input = { cols: 120, rows: 32 };
|
|
441
|
-
const session = existing?.status === "running"
|
|
442
|
-
? existing
|
|
443
|
-
: existing?.claudeSessionId
|
|
444
|
-
? await deps.sessionService.resumeRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input)
|
|
445
|
-
: await deps.sessionService.startRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input);
|
|
446
|
-
if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
state.reviewPromptDispatchedAt = now();
|
|
450
|
-
state.updatedAt = state.reviewPromptDispatchedAt;
|
|
451
|
-
await persistActiveState(taskRepoRoot, state);
|
|
452
|
-
await submitTerminalInput(deps.runtime, session.id, buildHarnessReviewPrompt(taskRepoRoot, state));
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
await failReview(taskRepoRoot, state, `Unable to start Harness Engineer memory review: ${errorMessage(error)}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
502
|
async function ensureWorkflowRoleSession(baseRepoRoot, taskSlug, role) {
|
|
459
503
|
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, taskSlug, role);
|
|
460
504
|
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
@@ -467,6 +511,20 @@ export function createAutoMemoryService(deps) {
|
|
|
467
511
|
}
|
|
468
512
|
return deps.sessionService.startRoleSession(baseRepoRoot, taskSlug, role, options);
|
|
469
513
|
}
|
|
514
|
+
async function snapshotArchitectPlanningCandidate(taskRepoRoot, runId) {
|
|
515
|
+
const sourcePath = resolveRepoPath(taskRepoRoot, ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH);
|
|
516
|
+
if (!(await deps.fs.pathExists(sourcePath))) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const snapshotPath = resolveRepoPath(taskRepoRoot, architectPlanningCandidateSnapshotPath(runId));
|
|
520
|
+
await deps.fs.writeText(snapshotPath, ensureTrailingNewline(await deps.fs.readText(sourcePath)));
|
|
521
|
+
}
|
|
522
|
+
async function findPlanningCandidateSnapshot(taskRepoRoot, runId) {
|
|
523
|
+
const relativePath = architectPlanningCandidateSnapshotPath(runId);
|
|
524
|
+
return await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, relativePath))
|
|
525
|
+
? relativePath
|
|
526
|
+
: undefined;
|
|
527
|
+
}
|
|
470
528
|
async function applyReviewedMemory(taskRepoRoot, state) {
|
|
471
529
|
try {
|
|
472
530
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
@@ -675,6 +733,8 @@ export function createAutoMemoryService(deps) {
|
|
|
675
733
|
updateFile,
|
|
676
734
|
revertRun,
|
|
677
735
|
retryFailedReview,
|
|
736
|
+
prepareTaskRetrospectiveReview,
|
|
737
|
+
cancelTaskRetrospectiveReview,
|
|
678
738
|
isRoleMemoryTurn,
|
|
679
739
|
handleRoleHook,
|
|
680
740
|
handleHarnessEngineerHook,
|
|
@@ -710,39 +770,30 @@ function toActiveReview(state) {
|
|
|
710
770
|
error: state.error
|
|
711
771
|
};
|
|
712
772
|
}
|
|
713
|
-
function buildRoleDraftPrompt(taskRepoRoot, state, draft) {
|
|
773
|
+
function buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath) {
|
|
714
774
|
const roleDefinition = MEMORY_FILE_DEFINITIONS.find((definition) => "role" in definition && definition.role === draft.role);
|
|
715
775
|
if (!roleDefinition) {
|
|
716
776
|
throw new Error(`Missing memory definition for role: ${draft.role}`);
|
|
717
777
|
}
|
|
718
|
-
|
|
778
|
+
const prompt = [
|
|
719
779
|
"[VCM Task Harness Review: Memory Proposal]",
|
|
720
780
|
"",
|
|
721
781
|
"Use the vcm-propose-memory skill to submit the assigned proposal.",
|
|
722
782
|
`Task worktree: ${taskRepoRoot}`,
|
|
723
783
|
`Current shared memory block: ${resolveRepoPath(taskRepoRoot, "CLAUDE.md")}`,
|
|
724
784
|
`Current role memory block: ${resolveRepoPath(taskRepoRoot, roleDefinition.path)}`,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
785
|
+
...(planningCandidatePath
|
|
786
|
+
? [
|
|
787
|
+
`Planning-session memory candidate: ${resolveRepoPath(taskRepoRoot, planningCandidatePath)}`,
|
|
788
|
+
"Review that candidate against final task evidence. Carry forward only facts that remain verified after implementation and testing."
|
|
789
|
+
]
|
|
790
|
+
: []),
|
|
791
|
+
`Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`
|
|
792
|
+
];
|
|
732
793
|
return [
|
|
733
|
-
|
|
734
|
-
"",
|
|
735
|
-
"Auto Memory is enabled. Review the role proposals and task evidence, then produce the complete next memory set.",
|
|
736
|
-
`Task worktree: ${taskRepoRoot}`,
|
|
737
|
-
`Role drafts: ${path.join(runRoot, "drafts")}`,
|
|
738
|
-
`Current memory snapshot: ${path.join(runRoot, "before")}`,
|
|
739
|
-
`Write the complete reviewed memory set to: ${path.join(runRoot, "after")}`,
|
|
794
|
+
...prompt,
|
|
740
795
|
"",
|
|
741
|
-
"
|
|
742
|
-
"Keep only verified, durable, reusable project knowledge. Merge duplicates, remove stale entries, and keep role-specific knowledge in the matching role file.",
|
|
743
|
-
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules.",
|
|
744
|
-
"Do not edit product code, active harness files, active memory blocks, or review metadata.",
|
|
745
|
-
"All existing files already exist in the after directory. Edit those files in place and end the turn when review is complete."
|
|
796
|
+
"End the turn after writing the draft."
|
|
746
797
|
].join("\n");
|
|
747
798
|
}
|
|
748
799
|
function requireMemoryFileDefinition(filePath) {
|
|
@@ -792,6 +843,12 @@ function hashMemorySet(memory) {
|
|
|
792
843
|
sha256(memory[definition.path] ?? "")
|
|
793
844
|
]));
|
|
794
845
|
}
|
|
846
|
+
function hasSubstantiveMemory(memory) {
|
|
847
|
+
return Object.values(memory).some((content) => {
|
|
848
|
+
const normalized = content.trim();
|
|
849
|
+
return Boolean(normalized && normalized !== "No accumulated project memory yet.");
|
|
850
|
+
});
|
|
851
|
+
}
|
|
795
852
|
function sameHashes(left, right) {
|
|
796
853
|
return MEMORY_FILE_DEFINITIONS.every((definition) => left[definition.path] === right[definition.path]);
|
|
797
854
|
}
|
|
@@ -133,7 +133,7 @@ export function createClaudeHookService(deps) {
|
|
|
133
133
|
if (!session) {
|
|
134
134
|
return completedHookResult(input, eventName);
|
|
135
135
|
}
|
|
136
|
-
const activeTask = deps.autoMemoryService
|
|
136
|
+
const activeTask = deps.autoMemoryService || deps.harnessFeedbackService
|
|
137
137
|
? (await deps.taskService.listTasks(context.project.repoRoot))
|
|
138
138
|
.find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
|
|
139
139
|
: undefined;
|
|
@@ -145,7 +145,17 @@ export function createClaudeHookService(deps) {
|
|
|
145
145
|
eventName
|
|
146
146
|
})
|
|
147
147
|
: false;
|
|
148
|
-
|
|
148
|
+
const memoryState = activeTask && deps.autoMemoryService
|
|
149
|
+
? await deps.autoMemoryService.getState(context.project.repoRoot, getTaskRuntimeRepoRoot(activeTask))
|
|
150
|
+
: undefined;
|
|
151
|
+
const retrospectiveHandled = activeTask
|
|
152
|
+
? await deps.harnessFeedbackService?.handleTaskRetrospectiveHook(context.project.repoRoot, {
|
|
153
|
+
taskSlug: activeTask.taskSlug,
|
|
154
|
+
eventName,
|
|
155
|
+
memoryReviewSucceeded: memoryState?.status !== "failed"
|
|
156
|
+
})
|
|
157
|
+
: false;
|
|
158
|
+
if (memoryHandled || retrospectiveHandled) {
|
|
149
159
|
return {
|
|
150
160
|
ok: true,
|
|
151
161
|
eventName,
|
|
@@ -49,7 +49,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
52
|
-
if (existingMarker) {
|
|
52
|
+
if (existingMarker && existingMarker.status !== "failed") {
|
|
53
53
|
throw new VcmError({
|
|
54
54
|
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
55
55
|
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
@@ -77,19 +77,82 @@ export function createHarnessFeedbackService(deps) {
|
|
|
77
77
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
78
78
|
const timestamp = now();
|
|
79
79
|
const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
80
|
-
|
|
80
|
+
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
81
|
+
const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
|
|
82
|
+
const marker = {
|
|
81
83
|
version: 1,
|
|
82
84
|
taskSlug,
|
|
83
85
|
trigger: input.trigger,
|
|
84
|
-
status: "
|
|
86
|
+
status: "running",
|
|
85
87
|
analysisPath,
|
|
86
88
|
finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
|
|
89
|
+
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
87
90
|
createdAt: timestamp,
|
|
88
91
|
updatedAt: timestamp
|
|
89
|
-
}
|
|
90
|
-
|
|
92
|
+
};
|
|
93
|
+
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
94
|
+
try {
|
|
95
|
+
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
96
|
+
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (memoryReview) {
|
|
100
|
+
await deps.autoMemoryService?.cancelTaskRetrospectiveReview(input.taskRepoRoot, memoryReview.runId);
|
|
101
|
+
}
|
|
102
|
+
const failedAt = now();
|
|
103
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
104
|
+
...marker,
|
|
105
|
+
status: "failed",
|
|
106
|
+
failedAt,
|
|
107
|
+
updatedAt: failedAt,
|
|
108
|
+
error: errorMessage(error)
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
91
112
|
return getState(repoRoot);
|
|
92
113
|
}
|
|
114
|
+
async function handleTaskRetrospectiveHook(repoRoot, input) {
|
|
115
|
+
const marker = await loadTaskRetrospectiveMarker(repoRoot, input.taskSlug);
|
|
116
|
+
if (!marker || (marker.status !== "running" && marker.status !== "triggered")) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const timestamp = now();
|
|
123
|
+
if (input.eventName === "StopFailure") {
|
|
124
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
125
|
+
...marker,
|
|
126
|
+
status: "failed",
|
|
127
|
+
failedAt: timestamp,
|
|
128
|
+
updatedAt: timestamp,
|
|
129
|
+
error: "Harness Engineer Task Harness Retrospective turn failed."
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const reportPath = resolveRepoPath(repoRoot, marker.analysisPath);
|
|
134
|
+
const reportReady = await deps.fs.pathExists(reportPath)
|
|
135
|
+
&& Boolean((await deps.fs.readText(reportPath)).trim());
|
|
136
|
+
if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
|
|
137
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
138
|
+
...marker,
|
|
139
|
+
status: "failed",
|
|
140
|
+
failedAt: timestamp,
|
|
141
|
+
updatedAt: timestamp,
|
|
142
|
+
error: !reportReady
|
|
143
|
+
? "Harness Engineer did not write the required Task Harness Retrospective report."
|
|
144
|
+
: "Task Harness Retrospective memory review failed."
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
149
|
+
...marker,
|
|
150
|
+
status: "completed",
|
|
151
|
+
completedAt: timestamp,
|
|
152
|
+
updatedAt: timestamp
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
93
156
|
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
94
157
|
return undefined;
|
|
95
158
|
}
|
|
@@ -162,12 +225,76 @@ export function createHarnessFeedbackService(deps) {
|
|
|
162
225
|
summary: metadata.summary
|
|
163
226
|
};
|
|
164
227
|
}
|
|
165
|
-
function buildTaskRetrospectivePrompt(repoRoot, analysisPath) {
|
|
228
|
+
function buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedbackPaths, memoryReview) {
|
|
229
|
+
const pendingFeedback = pendingFeedbackPaths.length > 0
|
|
230
|
+
? pendingFeedbackPaths.map((feedbackPath) => `- ${resolveRepoPath(repoRoot, feedbackPath)}`)
|
|
231
|
+
: ["none"];
|
|
166
232
|
return [
|
|
167
233
|
"[VCM Task Harness Retrospective]",
|
|
168
234
|
"",
|
|
169
235
|
"Review the completed task from the current active task worktree.",
|
|
170
236
|
"",
|
|
237
|
+
`Pending Feedback Directory: ${resolveRepoPath(repoRoot, PENDING_DIR)}`,
|
|
238
|
+
"",
|
|
239
|
+
"Pending Feedback:",
|
|
240
|
+
...pendingFeedback,
|
|
241
|
+
...(pendingFeedbackPaths.length > 0
|
|
242
|
+
? [
|
|
243
|
+
"",
|
|
244
|
+
"Process every listed feedback inside this retrospective. Record every disposition in the retrospective report, then delete the processed feedback files before ending the turn."
|
|
245
|
+
]
|
|
246
|
+
: []),
|
|
247
|
+
...(memoryReview
|
|
248
|
+
? [
|
|
249
|
+
"",
|
|
250
|
+
"Auto Memory Review:",
|
|
251
|
+
`Role drafts: ${memoryReview.roleDraftsPath}`,
|
|
252
|
+
`Current memory snapshot: ${memoryReview.currentMemoryPath}`,
|
|
253
|
+
...(memoryReview.planningCandidatePath
|
|
254
|
+
? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
|
|
255
|
+
: []),
|
|
256
|
+
`Write the complete reviewed memory set to: ${memoryReview.reviewedMemoryPath}`,
|
|
257
|
+
"",
|
|
258
|
+
"Review every memory candidate against final task evidence while performing this retrospective.",
|
|
259
|
+
"Each snapshot file contains only the matching <VCM-memory> block content. Edit every existing reviewed-memory file in place.",
|
|
260
|
+
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
261
|
+
"For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
|
|
262
|
+
"Complete this full existing-memory review even when every proposal says no-change.",
|
|
263
|
+
"Then evaluate every proposal, including its stated need, absence impact, and durable-document disposition.",
|
|
264
|
+
"Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
|
|
265
|
+
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
266
|
+
"Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
|
|
267
|
+
"Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
|
|
268
|
+
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
269
|
+
"Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
|
|
270
|
+
"Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
|
|
271
|
+
"",
|
|
272
|
+
"## Memory Review",
|
|
273
|
+
"Existing memory reviewed: complete",
|
|
274
|
+
"",
|
|
275
|
+
"### Proposal Dispositions",
|
|
276
|
+
...memoryReview.proposalRoles.map((role) => `- ${role}: accepted|rejected|no-change`),
|
|
277
|
+
"",
|
|
278
|
+
"### Existing Memory Decisions",
|
|
279
|
+
"#### Item 1",
|
|
280
|
+
"Target: shared",
|
|
281
|
+
"Existing: <exact existing memory entry>",
|
|
282
|
+
"Decision: retain",
|
|
283
|
+
"Reason: <why this decision is correct>",
|
|
284
|
+
"Impact if removed: <specific future role or task failure>",
|
|
285
|
+
"Durable doc disposition: memory",
|
|
286
|
+
"Durable doc path: none",
|
|
287
|
+
"Evidence: <current code, durable documentation, or final task evidence>",
|
|
288
|
+
"",
|
|
289
|
+
"### Existing Memory Changes",
|
|
290
|
+
"- retained: <summary or none>",
|
|
291
|
+
"- updated: <summary or none>",
|
|
292
|
+
"- removed: <summary or none>",
|
|
293
|
+
"",
|
|
294
|
+
"Reviewed memory set: complete"
|
|
295
|
+
]
|
|
296
|
+
: []),
|
|
297
|
+
"",
|
|
171
298
|
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
172
299
|
"End your turn after writing the result."
|
|
173
300
|
].join("\n");
|
|
@@ -190,7 +317,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
190
317
|
return deps.fs.readJson(markerPath);
|
|
191
318
|
}
|
|
192
319
|
async function persistTaskRetrospectiveMarker(repoRoot, marker) {
|
|
193
|
-
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(
|
|
320
|
+
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(marker.taskSlug));
|
|
194
321
|
await deps.fs.ensureDir(path.dirname(markerPath));
|
|
195
322
|
await deps.fs.writeJsonAtomic(markerPath, marker);
|
|
196
323
|
}
|
|
@@ -214,6 +341,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
214
341
|
getState,
|
|
215
342
|
sendPendingFeedback,
|
|
216
343
|
startTaskRetrospective,
|
|
344
|
+
handleTaskRetrospectiveHook,
|
|
217
345
|
assertHarnessEngineerAvailable
|
|
218
346
|
};
|
|
219
347
|
}
|
|
@@ -245,3 +373,6 @@ function sanitizeFeedbackId(value) {
|
|
|
245
373
|
function sha256(content) {
|
|
246
374
|
return createHash("sha256").update(content).digest("hex");
|
|
247
375
|
}
|
|
376
|
+
function errorMessage(error) {
|
|
377
|
+
return error instanceof Error ? error.message : String(error);
|
|
378
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const OPERATIONS = ["Add", "Update", "Remove"];
|
|
2
|
+
export function validateMemoryProposal(content) {
|
|
3
|
+
if (!/^# Memory Proposal\s*$/m.test(content)) {
|
|
4
|
+
return "is missing the # Memory Proposal heading";
|
|
5
|
+
}
|
|
6
|
+
const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
|
|
7
|
+
if (decisionMatches.length !== 1) {
|
|
8
|
+
return "must contain exactly one Decision: update or Decision: no-change field";
|
|
9
|
+
}
|
|
10
|
+
const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
|
|
11
|
+
const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
|
|
12
|
+
if (levelTwoHeadings.length !== OPERATIONS.length
|
|
13
|
+
|| sections.length !== OPERATIONS.length
|
|
14
|
+
|| sections.some((section, index) => section[1] !== OPERATIONS[index])) {
|
|
15
|
+
return "must contain only the Add, Update, and Remove sections, exactly once and in that order";
|
|
16
|
+
}
|
|
17
|
+
let itemCount = 0;
|
|
18
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
19
|
+
const section = sections[index];
|
|
20
|
+
const operation = section[1];
|
|
21
|
+
const bodyStart = (section.index ?? 0) + section[0].length;
|
|
22
|
+
const bodyEnd = index + 1 < sections.length
|
|
23
|
+
? sections[index + 1].index ?? content.length
|
|
24
|
+
: content.length;
|
|
25
|
+
const result = validateOperationBody(operation, content.slice(bodyStart, bodyEnd));
|
|
26
|
+
if (typeof result === "string") {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
itemCount += result;
|
|
30
|
+
}
|
|
31
|
+
const decision = decisionMatches[0][1];
|
|
32
|
+
if (decision === "no-change" && itemCount !== 0) {
|
|
33
|
+
return "uses Decision: no-change but contains a memory item";
|
|
34
|
+
}
|
|
35
|
+
if (decision === "update" && itemCount === 0) {
|
|
36
|
+
return "uses Decision: update without a structured memory item";
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function validateOperationBody(operation, rawBody) {
|
|
41
|
+
const body = rawBody.trim();
|
|
42
|
+
if (body === "none") {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
|
|
46
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
47
|
+
return `${operation} must contain none or one or more ### Item N blocks`;
|
|
48
|
+
}
|
|
49
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
50
|
+
const heading = itemHeadings[index];
|
|
51
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
52
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
53
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
54
|
+
: body.length;
|
|
55
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
56
|
+
const expectedPattern = operation === "Add"
|
|
57
|
+
? /^Target:[ \t]*(shared|current-role)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
58
|
+
: operation === "Update"
|
|
59
|
+
? /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
60
|
+
: /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
|
|
61
|
+
const fieldMatch = expectedPattern.exec(item);
|
|
62
|
+
if (!fieldMatch) {
|
|
63
|
+
const fields = operation === "Add"
|
|
64
|
+
? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
65
|
+
: operation === "Update"
|
|
66
|
+
? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
67
|
+
: "Target, Existing, and Evidence";
|
|
68
|
+
return `${operation} ${heading[0].trim()} must contain one-line ${fields} fields in that order`;
|
|
69
|
+
}
|
|
70
|
+
if (operation === "Add" || operation === "Update") {
|
|
71
|
+
const disposition = fieldMatch[operation === "Add" ? 5 : 6];
|
|
72
|
+
const durableDocPath = fieldMatch[operation === "Add" ? 6 : 7];
|
|
73
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
74
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
75
|
+
return `${operation} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return itemHeadings.length;
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
2
|
+
export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
3
|
+
export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
4
|
+
export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
|
|
5
|
+
export function architectPlanningCandidateSnapshotPath(runId) {
|
|
6
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function validateMemoryReviewReport(content, proposalRoles, hasExistingMemory = false) {
|
|
2
|
+
const memoryReview = /^## Memory Review\s*$/m.exec(content);
|
|
3
|
+
if (!memoryReview || memoryReview.index === undefined) {
|
|
4
|
+
return "is missing the ## Memory Review section";
|
|
5
|
+
}
|
|
6
|
+
const sectionStart = memoryReview.index + memoryReview[0].length;
|
|
7
|
+
const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
|
|
8
|
+
const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
|
|
9
|
+
if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
10
|
+
return "must declare Existing memory reviewed: complete";
|
|
11
|
+
}
|
|
12
|
+
if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
13
|
+
return "must declare Reviewed memory set: complete";
|
|
14
|
+
}
|
|
15
|
+
const dispositions = extractReportSubsection(section, "Proposal Dispositions", "Existing Memory Decisions");
|
|
16
|
+
if (dispositions === undefined) {
|
|
17
|
+
return "is missing the Proposal Dispositions subsection";
|
|
18
|
+
}
|
|
19
|
+
for (const role of proposalRoles) {
|
|
20
|
+
const matches = dispositions.match(new RegExp(`^- ${escapeRegExp(role)}:[ \\t]*(accepted|rejected|no-change)[ \\t]*$`, "gm"));
|
|
21
|
+
if (matches?.length !== 1) {
|
|
22
|
+
return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
|
|
26
|
+
if (existingDecisions === undefined) {
|
|
27
|
+
return "is missing the Existing Memory Decisions subsection";
|
|
28
|
+
}
|
|
29
|
+
const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
|
|
30
|
+
if (existingDecisionError) {
|
|
31
|
+
return existingDecisionError;
|
|
32
|
+
}
|
|
33
|
+
const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
|
|
34
|
+
if (existingChanges === undefined) {
|
|
35
|
+
return "is missing the Existing Memory Changes subsection";
|
|
36
|
+
}
|
|
37
|
+
for (const field of ["retained", "updated", "removed"]) {
|
|
38
|
+
const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
|
|
39
|
+
if (matches?.length !== 1) {
|
|
40
|
+
return `must record exactly one non-empty ${field} summary`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function validateExistingMemoryDecisions(content, hasExistingMemory) {
|
|
46
|
+
const body = content.trim();
|
|
47
|
+
if (body === "none") {
|
|
48
|
+
return hasExistingMemory
|
|
49
|
+
? "Existing Memory Decisions cannot be none while substantive existing memory is present"
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
const itemHeadings = [...body.matchAll(/^#### Item \d+[ \t]*$/gm)];
|
|
53
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
54
|
+
return "Existing Memory Decisions must contain none or one or more #### Item N blocks";
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
57
|
+
const heading = itemHeadings[index];
|
|
58
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
59
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
60
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
61
|
+
: body.length;
|
|
62
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
63
|
+
const match = /^Target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(retain|update|remove|move-to-durable-doc)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if removed:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return `Existing Memory Decisions ${heading[0].trim()} must contain one-line Target, Existing, Decision, Reason, Impact if removed, Durable doc disposition, Durable doc path, and Evidence fields in that order`;
|
|
66
|
+
}
|
|
67
|
+
const disposition = match[6];
|
|
68
|
+
const durableDocPath = match[7];
|
|
69
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
70
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
71
|
+
return `Existing Memory Decisions ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function extractReportSubsection(content, heading, nextHeading) {
|
|
77
|
+
const start = new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
|
|
78
|
+
if (!start || start.index === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const bodyStart = start.index + start[0].length;
|
|
82
|
+
if (nextHeading) {
|
|
83
|
+
const end = new RegExp(`^### ${escapeRegExp(nextHeading)}\\s*$`, "m")
|
|
84
|
+
.exec(content.slice(bodyStart));
|
|
85
|
+
return end?.index === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: content.slice(bodyStart, bodyStart + end.index);
|
|
88
|
+
}
|
|
89
|
+
const reviewedSet = /^Reviewed memory set:/m.exec(content.slice(bodyStart));
|
|
90
|
+
return content.slice(bodyStart, reviewedSet?.index === undefined ? content.length : bodyStart + reviewedSet.index);
|
|
91
|
+
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
@@ -116,7 +116,8 @@ ${renderRoleMemoryRules("architect")}
|
|
|
116
116
|
#### Planning Completion
|
|
117
117
|
|
|
118
118
|
- After the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready, use the \`restart-architect\` skill before writing the completed Architect-to-PM route message.
|
|
119
|
-
-
|
|
119
|
+
- If VCM returns a \`memoryCandidatePath\`, use \`vcm-propose-memory\` to write the planning-session memory candidate to that path before routing. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
|
|
120
|
+
- After the required candidate is written, write the route message with both architecture artifacts and the plan, then end the turn. VCM keeps this session for any architecture-plan Gate revision and restarts it only after the Gate is accepted. Do not wait for or inspect the replacement session.
|
|
120
121
|
|
|
121
122
|
### Complete Task Planning
|
|
122
123
|
|
|
@@ -7,7 +7,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
7
7
|
- Project-manager uses \`vcm-task-state\` to declare the current workflow checkpoint. This state is recoverable context only; flow rules and task artifacts remain authoritative.
|
|
8
8
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
9
9
|
- Use \`vcm-report-harness-issue\` when you notice a reusable VCM harness problem. Record feedback; do not contact Harness Engineer directly.
|
|
10
|
-
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns
|
|
10
|
+
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns an exact memory proposal or candidate path.
|
|
11
11
|
- Only the user may approve scope reduction, skipped required validation, Gate Review skip or override, skipped required docs sync, accepted unresolved task-scope risk, or weakening of baseline Harness rules. PM may record and route the user's approval but cannot grant it.
|
|
12
12
|
- Project-manager runs \`vcm-gate-review\` unconditionally at every Gate Review trigger point and on VCM Gate Review callbacks; the tool reports the authoritative enable state.
|
|
13
13
|
|
|
@@ -38,20 +38,23 @@ You are not part of the task workflow round state.
|
|
|
38
38
|
permitted bootstrap edits directly in the active task worktree and commit them
|
|
39
39
|
yourself.
|
|
40
40
|
- Retrospective Mode: analyze a completed task for reusable harness problems.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
phase of Task Harness Review, review role proposals and write only the review
|
|
44
|
-
output files assigned by VCM.
|
|
41
|
+
When the assigned prompt includes Auto Memory Review, also review the memory
|
|
42
|
+
proposals and write only the reviewed-memory output files assigned by VCM.
|
|
45
43
|
- VCM Feedback Mode: draft VCM product, installer, UI, or fixed-template issue
|
|
46
44
|
feedback. Do not submit without explicit in-session user authorization.
|
|
47
45
|
|
|
48
46
|
## Change Policy
|
|
49
47
|
|
|
50
|
-
- Apply edits only in Bootstrap Apply Mode,
|
|
51
|
-
explicitly asks you to apply an
|
|
48
|
+
- Apply edits only in Bootstrap Apply Mode, to assigned reviewed-memory output
|
|
49
|
+
files during Retrospective Mode, or when VCM explicitly asks you to apply an
|
|
50
|
+
approved harness change.
|
|
52
51
|
- When applying edits, work only in the active task worktree named by VCM. Do not
|
|
53
52
|
edit the base repository root unless VCM explicitly says so.
|
|
54
|
-
- In Proposal Mode
|
|
53
|
+
- In Proposal Mode, do not edit files.
|
|
54
|
+
- In Retrospective Mode, write the assigned retrospective report and, only when
|
|
55
|
+
Auto Memory Review is included in the prompt, the assigned reviewed-memory
|
|
56
|
+
output files. After every assigned pending feedback has a recorded
|
|
57
|
+
disposition, delete those processed feedback files.
|
|
55
58
|
- Commit every applied harness change yourself before ending your turn.
|
|
56
59
|
- Do not overwrite VCM fixed managed blocks.
|
|
57
60
|
- Keep project-specific customization outside VCM managed blocks.
|
|
@@ -64,11 +67,26 @@ You are not part of the task workflow round state.
|
|
|
64
67
|
## Memory Management
|
|
65
68
|
|
|
66
69
|
- Own VCM-managed project memory in the root and role \`<VCM-memory>\` blocks.
|
|
67
|
-
- When Auto Memory is disabled, do not request proposals
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
- When Auto Memory is disabled, do not request proposals or update memory.
|
|
71
|
+
- During a Retrospective that includes Auto Memory Review, first inspect every
|
|
72
|
+
entry in every current memory snapshot. Verify each entry against current
|
|
73
|
+
code, durable documentation, and final task evidence. For every substantive
|
|
74
|
+
entry, decide whether to retain, update, remove, or move it to a durable
|
|
75
|
+
document; record the decision reason, the impact of removing it, and whether
|
|
76
|
+
memory or a durable document is the correct source. Complete this full review
|
|
77
|
+
even when every proposal says \`no-change\`.
|
|
78
|
+
- After reviewing existing memory, verify every role proposal against task
|
|
79
|
+
evidence, including any Architect planning-session candidate assigned by VCM.
|
|
80
|
+
Independently verify the stated need, absence impact, and durable-document
|
|
81
|
+
disposition. Treat every candidate as a proposal rather than authority, merge
|
|
82
|
+
duplicates, remove stale entries, and keep role-specific knowledge in the
|
|
83
|
+
matching role memory output.
|
|
84
|
+
- Do not keep the full content in memory when a durable document is the correct
|
|
85
|
+
source. Use a short memory reference only when the role needs that document
|
|
86
|
+
pointer across tasks.
|
|
87
|
+
- Record every proposal disposition and the retained, updated, and removed
|
|
88
|
+
existing-memory decisions and summary in the exact Memory Review report block
|
|
89
|
+
assigned by VCM.
|
|
72
90
|
- Do not record task narrative, temporary state, unverified conclusions, or
|
|
73
91
|
Harness rules in memory.
|
|
74
92
|
- Edit only the review output paths assigned by VCM. Do not edit active
|
|
@@ -91,6 +109,28 @@ context, validation reports, Gate Review reports, final acceptance artifacts,
|
|
|
91
109
|
memory drafts, applied memory diffs, current memory, and user corrections during
|
|
92
110
|
the task.
|
|
93
111
|
|
|
112
|
+
Pending Harness Feedback is part of the retrospective, not a separate phase.
|
|
113
|
+
At the start of the retrospective, read every feedback file assigned by VCM
|
|
114
|
+
from \`.ai/vcm/harness-feedback/pending/\`.
|
|
115
|
+
|
|
116
|
+
For each pending feedback:
|
|
117
|
+
|
|
118
|
+
- verify it against the current harness, task evidence, and project behavior
|
|
119
|
+
- decide whether it is confirmed, rejected, duplicate, or already covered
|
|
120
|
+
- record the feedback path, decision, evidence, impact, and required action in
|
|
121
|
+
the retrospective report
|
|
122
|
+
|
|
123
|
+
Process every assigned feedback before completing the retrospective. A
|
|
124
|
+
feedback item is processed even when it is rejected or already covered.
|
|
125
|
+
|
|
126
|
+
Write the complete retrospective report before deleting any feedback file.
|
|
127
|
+
After the report contains a disposition for every assigned feedback, delete
|
|
128
|
+
those feedback files from \`.ai/vcm/harness-feedback/pending/\`.
|
|
129
|
+
|
|
130
|
+
Do not delete a feedback file unless its disposition is already recorded in
|
|
131
|
+
the retrospective report. Do not leave an assigned feedback file pending after
|
|
132
|
+
its disposition has been recorded.
|
|
133
|
+
|
|
94
134
|
For each finding, decide whether it is:
|
|
95
135
|
|
|
96
136
|
- a reusable harness problem that should be fixed
|
|
@@ -108,6 +148,7 @@ Do not edit harness files during retrospective analysis. Write a concise analysi
|
|
|
108
148
|
- impact
|
|
109
149
|
- recommended harness change, or reason no harness change is needed
|
|
110
150
|
- affected roles, skills, tools, or docs
|
|
151
|
+
- pending feedback path and disposition
|
|
111
152
|
|
|
112
153
|
## VCM Feedback
|
|
113
154
|
|
|
@@ -9,7 +9,12 @@ Run:
|
|
|
9
9
|
.ai/tools/request-architect-restart
|
|
10
10
|
\`\`\`
|
|
11
11
|
|
|
12
|
-
If VCM reports \`scheduled
|
|
12
|
+
If VCM reports \`scheduled\` with a non-empty \`memoryCandidatePath\`, use
|
|
13
|
+
\`vcm-propose-memory\` to write a planning-session memory candidate to that exact
|
|
14
|
+
path before writing the completed route. This candidate is provisional input for
|
|
15
|
+
the later Auto Memory review; it does not edit active memory.
|
|
16
|
+
|
|
17
|
+
Then write the completed Architect-to-PM route message and end the turn. VCM keeps the current Architect session through any architecture-plan Gate revision rounds and restarts it only after the route is accepted by PM and that Gate is approved or explicitly excepted.
|
|
13
18
|
|
|
14
19
|
Do not use this skill for incomplete planning, user clarification, Debug Mode, Architecture Diagnosis Mode, or docs sync.`;
|
|
15
20
|
}
|
|
@@ -54,7 +59,12 @@ def main():
|
|
|
54
59
|
try:
|
|
55
60
|
with urllib.request.urlopen(request, timeout=5) as response:
|
|
56
61
|
payload = json.loads(response.read().decode("utf-8"))
|
|
57
|
-
emit(
|
|
62
|
+
emit(
|
|
63
|
+
payload.get("status", "scheduled"),
|
|
64
|
+
taskSlug=task_slug,
|
|
65
|
+
sessionId=payload.get("sessionId"),
|
|
66
|
+
memoryCandidatePath=payload.get("memoryCandidatePath"),
|
|
67
|
+
)
|
|
58
68
|
return 0
|
|
59
69
|
except urllib.error.HTTPError as error:
|
|
60
70
|
try:
|
|
@@ -5,8 +5,8 @@ role turns. Update reviewed memory only through the output paths assigned by VCM
|
|
|
5
5
|
or explicit user edits in Harness Studio. When Auto Memory is disabled, do not
|
|
6
6
|
initiate memory proposals, reviews, or updates.`
|
|
7
7
|
: `Treat the \`<VCM-memory>\` block in this role definition as read-only. Only
|
|
8
|
-
when VCM explicitly
|
|
9
|
-
\`vcm-propose-memory\` and write
|
|
8
|
+
when VCM explicitly assigns a memory proposal or candidate path, use
|
|
9
|
+
\`vcm-propose-memory\` and write that exact path.`;
|
|
10
10
|
return `### Role Memory
|
|
11
11
|
|
|
12
12
|
The \`<VCM-memory>\` block in this role definition is accumulated project context,
|
|
@@ -1,33 +1,81 @@
|
|
|
1
1
|
export function renderVcmProposeMemorySkillRules() {
|
|
2
|
-
return `Use this skill only when VCM explicitly requests a memory proposal
|
|
3
|
-
|
|
2
|
+
return `Use this skill only when VCM explicitly requests a memory proposal or
|
|
3
|
+
planning-session memory candidate and provides an exact path.
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
7
|
- Treat every \`<VCM-memory>\` block as read-only. This skill creates a proposal;
|
|
8
8
|
it never edits active memory.
|
|
9
|
-
- Write only to the exact
|
|
10
|
-
\`.ai/vcm/memory-review/runs/<run-id>/drafts/\`
|
|
11
|
-
-
|
|
9
|
+
- Write only to the exact path assigned by VCM. It must be either a role draft
|
|
10
|
+
under \`.ai/vcm/memory-review/runs/<run-id>/drafts/\` or a planning candidate
|
|
11
|
+
under \`.ai/vcm/memory-review/candidates/\` in the active task worktree.
|
|
12
|
+
- If VCM did not provide a path, do not create a proposal.
|
|
12
13
|
- Propose only verified, durable, reusable project knowledge supported by task
|
|
13
14
|
evidence.
|
|
15
|
+
- Target shared project knowledge to \`shared\`. Target knowledge used only by
|
|
16
|
+
the current role to \`current-role\`.
|
|
17
|
+
- For every add or update, explain why the memory is necessary, what future
|
|
18
|
+
impact its absence would have, and whether the knowledge belongs in memory,
|
|
19
|
+
a durable document, or a short memory reference to a durable document.
|
|
20
|
+
Use exactly \`memory\`, \`durable-doc\`, or \`memory-reference\`. Use
|
|
21
|
+
\`Durable doc path: none\` with \`memory\` and an actual path with either
|
|
22
|
+
durable-document disposition.
|
|
14
23
|
- Do not record task narrative, temporary state, unverified conclusions, or
|
|
15
24
|
Harness rules.
|
|
16
25
|
- Do not edit handoff artifacts or route messages from this skill.
|
|
17
26
|
|
|
18
27
|
## Draft Format
|
|
19
28
|
|
|
29
|
+
Use this exact format for \`Decision: no-change\`:
|
|
30
|
+
|
|
20
31
|
\`\`\`markdown
|
|
21
32
|
# Memory Proposal
|
|
22
|
-
Decision:
|
|
33
|
+
Decision: no-change
|
|
23
34
|
|
|
24
35
|
## Add
|
|
36
|
+
none
|
|
25
37
|
|
|
26
38
|
## Update
|
|
39
|
+
none
|
|
27
40
|
|
|
28
41
|
## Remove
|
|
42
|
+
none
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
For \`Decision: update\`, use one or more numbered items and write every field
|
|
46
|
+
on one line. Use \`none\` as the complete body of an operation section that has
|
|
47
|
+
no item:
|
|
48
|
+
|
|
49
|
+
\`\`\`markdown
|
|
50
|
+
# Memory Proposal
|
|
51
|
+
Decision: update
|
|
29
52
|
|
|
30
|
-
##
|
|
53
|
+
## Add
|
|
54
|
+
### Item 1
|
|
55
|
+
Target: shared
|
|
56
|
+
Content: <new memory entry>
|
|
57
|
+
Reason: <why this must remain available across tasks>
|
|
58
|
+
Impact if absent: <specific future role or task failure>
|
|
59
|
+
Durable doc disposition: memory
|
|
60
|
+
Durable doc path: none
|
|
61
|
+
Evidence: <task artifact, code, or durable documentation>
|
|
62
|
+
|
|
63
|
+
## Update
|
|
64
|
+
### Item 1
|
|
65
|
+
Target: current-role
|
|
66
|
+
Existing: <exact existing memory entry>
|
|
67
|
+
Content: <replacement memory entry>
|
|
68
|
+
Reason: <why the replacement must remain available across tasks>
|
|
69
|
+
Impact if absent: <specific future role or task failure>
|
|
70
|
+
Durable doc disposition: memory-reference
|
|
71
|
+
Durable doc path: docs/ARCHITECTURE.md
|
|
72
|
+
Evidence: <task artifact, code, or durable documentation>
|
|
73
|
+
|
|
74
|
+
## Remove
|
|
75
|
+
### Item 1
|
|
76
|
+
Target: current-role
|
|
77
|
+
Existing: <exact existing memory entry>
|
|
78
|
+
Evidence: <task artifact, code, or durable documentation>
|
|
31
79
|
\`\`\`
|
|
32
80
|
|
|
33
81
|
Use \`Decision: no-change\` when the completed task produced no qualifying
|