agentera 3.0.0-dev.72 → 3.0.0-dev.75
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 +111 -0
- package/bundle/.agentera-build-source.json +8 -0
- package/bundle/CHANGELOG.md +17 -17
- package/bundle/extract-corpus-parity.json +1 -1
- package/bundle/references/adapters/package-publication.json +49 -53
- package/bundle/references/adapters/package-registry.yaml +5 -1
- package/bundle/references/adapters/package-surface-characterization.md +6 -4
- package/bundle/references/analysis/evidence-tier-authority.yaml +45 -4
- package/bundle/references/analysis/personal-glossary-evaluation-authority.yaml +175 -0
- package/bundle/references/analysis/personal-glossary-evaluation-corpus.yaml +291 -0
- package/bundle/references/analysis/personal-glossary-holdout.yaml +192 -0
- package/bundle/references/analysis/verification-policy.yaml +8 -13
- package/bundle/references/artifacts/glossary-entry-contract.yaml +1533 -35
- package/bundle/references/cli/update-channels.yaml +2 -2
- package/bundle/references/cli/vocabulary.md +1 -1
- package/bundle/references/meta/retained-reference-authority.yaml +16 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +11 -7
- package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +6 -3
- package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +13 -7
- package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +5 -0
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +7 -0
- package/dist/.agentera-build-source.json +8 -0
- package/dist/analytics/extractCorpus/copilotSessions.js +13 -4
- package/dist/analytics/extractCorpus/core.js +223 -3
- package/dist/analytics/extractCorpus/corpus.js +18 -1
- package/dist/analytics/extractCorpus/cursorSessions.js +22 -4
- package/dist/analytics/extractCorpus/evidenceTiers.js +123 -17
- package/dist/analytics/extractCorpus/filesystemSources.js +4 -0
- package/dist/analytics/extractCorpus/index.js +2 -2
- package/dist/analytics/extractCorpus/jsonlSessions.js +52 -3
- package/dist/analytics/extractCorpus/sqliteSessions.js +12 -3
- package/dist/analytics/personalGlossaryAdmission.js +26 -46
- package/dist/analytics/personalGlossaryCandidateProjection.js +692 -0
- package/dist/analytics/personalGlossaryCandidateProjectionExcerpts.js +144 -0
- package/dist/analytics/personalGlossaryCandidateReadView.js +23 -0
- package/dist/analytics/personalGlossaryCurrentGeneration.js +25 -0
- package/dist/analytics/personalGlossaryDecision.js +203 -0
- package/dist/analytics/personalGlossaryExplicit.js +788 -0
- package/dist/analytics/personalGlossaryExplicitMining.js +285 -0
- package/dist/analytics/personalGlossaryExplicitScope.js +41 -0
- package/dist/analytics/personalGlossaryExplicitSegments.js +418 -0
- package/dist/analytics/personalGlossaryExplicitTypes.js +26 -0
- package/dist/analytics/personalGlossaryLexicalClassifier.js +175 -0
- package/dist/analytics/personalGlossaryProfile.js +13 -3
- package/dist/analytics/personalGlossaryRecurrence.js +618 -0
- package/dist/analytics/personalGlossaryReviewRecordStorage.js +428 -0
- package/dist/analytics/personalGlossaryReviewRecords.js +332 -0
- package/dist/capabilities/index.js +5 -1
- package/dist/capabilities/profile/instructions.js +50 -15
- package/dist/cli/commands/personalGlossaryCandidateReads.js +744 -0
- package/dist/cli/commands/personalGlossaryDecision.js +349 -0
- package/dist/cli/commands/personalGlossaryPublish.js +484 -0
- package/dist/cli/commands/personalGlossaryReviewRecordReads.js +209 -0
- package/dist/cli/commands/personalGlossaryReviewRecords.js +623 -0
- package/dist/cli/commands/prime/briefOrientation.js +118 -19
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/schema.js +179 -0
- package/dist/cli/commands/verify.js +51 -9
- package/dist/cli/dispatch/lifecycle.js +51 -6
- package/dist/cli/help.js +48 -2
- package/dist/cli/migrationRequired.js +4 -1
- package/dist/core/atomicWriter.js +4 -1
- package/dist/core/developmentInvocation.js +4 -1
- package/dist/core/yaml.js +53 -1
- package/dist/eval/glossaryEvaluation.js +751 -0
- package/dist/eval/glossaryEvaluationProcess.js +29 -0
- package/dist/eval/glossaryEvaluationRunner.js +571 -0
- package/dist/eval/glossaryEvaluationSuccessReport.js +297 -0
- package/dist/registries/activationTuples.js +31 -3
- package/dist/registries/evidenceTierContract.js +4 -0
- package/dist/registries/explicitSegmentGrammarContract.js +619 -0
- package/dist/registries/glossaryCandidateContractPaths.js +6 -0
- package/dist/registries/glossaryCandidateContracts.js +820 -0
- package/dist/registries/glossaryCandidateDecisionAuthority.js +119 -0
- package/dist/registries/glossaryCandidateDecisionContract.js +62 -0
- package/dist/registries/glossaryCandidateProjectionAuthority.js +197 -0
- package/dist/registries/glossaryCandidateProjectionContract.js +104 -0
- package/dist/registries/glossaryEntryContract.js +105 -103
- package/dist/registries/glossaryEntryTemporal.js +12 -0
- package/dist/registries/glossaryMiningAuthority.js +764 -0
- package/dist/registries/glossaryProfileFullContract.js +24 -0
- package/dist/registries/glossaryReviewRecordsAuthority.js +426 -0
- package/dist/registries/glossaryReviewRecordsContract.js +129 -0
- package/dist/registries/glossaryTermIdentity.js +122 -0
- package/dist/registries/packagePublication.js +3 -3
- package/dist/registries/personalGlossaryContracts.js +1 -0
- package/dist/release/releaseMetadata.js +15 -8
- package/dist/upgrade/npxPlatformStatus.js +2 -1
- package/dist/upgrade/projectIntegration.js +7 -1
- package/dist/validate/activationArtifactEvidence.js +18 -4
- package/dist/validate/activationEvidenceManifest.js +101 -22
- package/package.json +2 -4
- package/dist/cli/commands/personalGlossary.js +0 -176
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { readCurrentPersonalGlossaryCandidateProjection } from "./personalGlossaryCurrentGeneration.js";
|
|
3
|
+
import { decidePersonalGlossaryCandidate } from "./personalGlossaryDecision.js";
|
|
4
|
+
import { createGlossaryReviewRecord, validateGlossaryAdmissionDecision, validateGlossaryHostClassificationReceipt, validateGlossaryReviewRecord, } from "../registries/glossaryCandidateContracts.js";
|
|
5
|
+
import { personalReviewApprovalReceiptDigest, personalReviewApprovalReplayStatus, personalReviewDispositionLifecycle, validatePersonalReviewApprovalReceipt, } from "../registries/glossaryMiningAuthority.js";
|
|
6
|
+
import { canonicalGlossaryJson, compareGlossaryUnicodeStrings } from "../registries/glossaryTermIdentity.js";
|
|
7
|
+
import { activeReplayDigestMap, currentReplayIndex, ensurePersonalGlossaryReviewRecordsPrivate, isCurrentReviewRecord, isCurrentReviewStore, isLegacyReviewStore, makePersonalGlossaryReviewStore, migrateLegacyPendingReviewRecord, personalGlossaryReviewRecordsPath, personalGlossaryReviewRecordsStorageContract, personalGlossaryTrustedLocalHostPath, privateWritePersonalGlossaryReviewRecords, readPersonalGlossaryReviewRecords, readPersonalGlossaryTrustedLocalHost, readablePersonalGlossaryReviewRecord, recordMatchesProjection, replayDigestMap, replayNonceDigest, reviewExpiry, reviewIdentity, reviewScope, sealPersonalGlossaryReviewRecord, terminalReviewRecordExpired, validPersonalGlossaryReviewGenerationBinding, validPersonalGlossaryReviewMetadataBinding, } from "./personalGlossaryReviewRecordStorage.js";
|
|
8
|
+
export { personalGlossaryReviewRecordsPath, personalGlossaryTrustedLocalHostPath, readPersonalGlossaryReviewRecords, validPersonalGlossaryReviewGenerationBinding, validPersonalGlossaryReviewMetadataBinding, };
|
|
9
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
10
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
11
|
+
function mapping(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function timestamp(value) {
|
|
15
|
+
return typeof value === "string" && TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value));
|
|
16
|
+
}
|
|
17
|
+
function digest(value) {
|
|
18
|
+
return typeof value === "string" && SHA256.test(value);
|
|
19
|
+
}
|
|
20
|
+
function exactCandidate(candidates, decision) {
|
|
21
|
+
return candidates.find((candidate) => candidate.capsule.candidate_id === decision.candidate_id &&
|
|
22
|
+
candidate.capsule.candidate_revision === decision.candidate_revision &&
|
|
23
|
+
candidate.capsule.capsule_sha256 === decision.candidate_capsule_sha256 &&
|
|
24
|
+
candidate.capsule.generation === decision.generation &&
|
|
25
|
+
candidate.capsule.policy_version === decision.policy_version) ?? null;
|
|
26
|
+
}
|
|
27
|
+
function pendingRecord(decision, scope, queuedAt, reopenReason) {
|
|
28
|
+
const body = {
|
|
29
|
+
schema_version: "agentera.personalGlossaryReviewRecord.v2",
|
|
30
|
+
owner: "current_user",
|
|
31
|
+
candidate_id: decision.candidate_id,
|
|
32
|
+
candidate_revision: decision.candidate_revision,
|
|
33
|
+
candidate_capsule_sha256: decision.candidate_capsule_sha256,
|
|
34
|
+
candidate_projection_sha256: decision.candidate_projection_sha256,
|
|
35
|
+
host_receipt_sha256: decision.host_receipt_sha256,
|
|
36
|
+
cli_decision_sha256: decision.decision_sha256,
|
|
37
|
+
semantic_fingerprint: decision.semantic_fingerprint,
|
|
38
|
+
generation: decision.generation,
|
|
39
|
+
policy_version: decision.policy_version,
|
|
40
|
+
scope,
|
|
41
|
+
reason: decision.reason,
|
|
42
|
+
status: "pending",
|
|
43
|
+
disposition: null,
|
|
44
|
+
review_record: null,
|
|
45
|
+
reopen_reason: reopenReason,
|
|
46
|
+
queued_at: queuedAt,
|
|
47
|
+
terminal_at: null,
|
|
48
|
+
expires_at: null,
|
|
49
|
+
};
|
|
50
|
+
return sealPersonalGlossaryReviewRecord({ ...body, review_id: reviewIdentity(body) });
|
|
51
|
+
}
|
|
52
|
+
function suppresses(record) {
|
|
53
|
+
return record.disposition === "reject" || record.disposition === "defer";
|
|
54
|
+
}
|
|
55
|
+
function sameSuppressionBinding(previous, current) {
|
|
56
|
+
return previous.candidate_id === current.candidate_id &&
|
|
57
|
+
previous.semantic_fingerprint === current.semantic_fingerprint &&
|
|
58
|
+
previous.scope === current.scope && previous.policy_version === current.policy_version;
|
|
59
|
+
}
|
|
60
|
+
function reopeningReason(previous, current) {
|
|
61
|
+
if (previous.candidate_id !== current.candidate_id)
|
|
62
|
+
return null;
|
|
63
|
+
if (previous.policy_version !== current.policy_version)
|
|
64
|
+
return "policy_changed";
|
|
65
|
+
if (previous.scope !== current.scope)
|
|
66
|
+
return "scope_changed";
|
|
67
|
+
if (previous.semantic_fingerprint !== current.semantic_fingerprint)
|
|
68
|
+
return "meaning_changed";
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function latestComparableRecord(records, candidateId, now) {
|
|
72
|
+
return records.filter(isCurrentReviewRecord).filter((record) => record.candidate_id === candidateId && record.disposition !== null && !terminalReviewRecordExpired(record, now)).sort((left, right) => Date.parse(right.queued_at) - Date.parse(left.queued_at) || compareGlossaryUnicodeStrings(left.review_id, right.review_id))[0] ?? null;
|
|
73
|
+
}
|
|
74
|
+
function publicationAuthorization(record) {
|
|
75
|
+
return record.review_record && ["accept", "correct"].includes(String(record.disposition))
|
|
76
|
+
? { review_id: record.review_id, review_record_sha256: record.review_record.record_sha256 }
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
export function currentPersonalGlossaryReviewRecords(options = {}, now = new Date().toISOString()) {
|
|
80
|
+
if (!timestamp(now))
|
|
81
|
+
throw new TypeError("review read time must be an ISO timestamp");
|
|
82
|
+
const current = readPersonalGlossaryReviewRecords(options);
|
|
83
|
+
if (current.status === "missing")
|
|
84
|
+
return { status: "current", records: [], expired_records: 0, stale_records: 0 };
|
|
85
|
+
if (current.status !== "current" || current.store === null)
|
|
86
|
+
return { status: current.status, records: [], expired_records: 0, stale_records: 0 };
|
|
87
|
+
const projection = readCurrentPersonalGlossaryCandidateProjection(options);
|
|
88
|
+
// Keep stale metadata observable only through the existing degraded count.
|
|
89
|
+
if (projection.status === "projection_stale") {
|
|
90
|
+
return {
|
|
91
|
+
status: "current",
|
|
92
|
+
records: [],
|
|
93
|
+
expired_records: 0,
|
|
94
|
+
stale_records: current.store.records.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (projection.status !== "current" || projection.projection === null)
|
|
98
|
+
return { status: "projection_unavailable", records: [], expired_records: 0, stale_records: 0 };
|
|
99
|
+
let expiredRecords = 0;
|
|
100
|
+
let staleRecords = 0;
|
|
101
|
+
const records = current.store.records.filter((record) => {
|
|
102
|
+
if (terminalReviewRecordExpired(record, now)) {
|
|
103
|
+
expiredRecords += 1;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (!recordMatchesProjection(record, projection.projection)) {
|
|
107
|
+
staleRecords += 1;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}).map(readablePersonalGlossaryReviewRecord);
|
|
112
|
+
return { status: "current", records, expired_records: expiredRecords, stale_records: staleRecords };
|
|
113
|
+
}
|
|
114
|
+
/** Queue one current review-required decision without accepting a user disposition. */
|
|
115
|
+
export function queuePersonalGlossaryReviewRecord(input) {
|
|
116
|
+
const queuedAt = input.now ?? new Date().toISOString();
|
|
117
|
+
if (!timestamp(queuedAt))
|
|
118
|
+
throw new TypeError("review queue time must be an ISO timestamp");
|
|
119
|
+
if (!mapping(input.receipt))
|
|
120
|
+
return { status: "decision_not_review_required", reason: "receipt_invalid", record: null, reopen_reason: null };
|
|
121
|
+
const options = { env: input.env, platform: input.platform };
|
|
122
|
+
const decisionResult = decidePersonalGlossaryCandidate(input.receipt, options);
|
|
123
|
+
if (decisionResult.status !== "review_required" || decisionResult.decision === null)
|
|
124
|
+
return { status: "decision_not_review_required", reason: decisionResult.reason, record: null, reopen_reason: null };
|
|
125
|
+
const decision = decisionResult.decision;
|
|
126
|
+
const projection = readCurrentPersonalGlossaryCandidateProjection(options);
|
|
127
|
+
if (projection.status !== "current" || projection.projection === null || projection.projection.projection_sha256 !== decision.candidate_projection_sha256)
|
|
128
|
+
return { status: "current_binding_mismatch", reason: "projection_unavailable", record: null, reopen_reason: null };
|
|
129
|
+
const candidate = exactCandidate(projection.projection.candidates, decision);
|
|
130
|
+
const scope = reviewScope(input.receipt);
|
|
131
|
+
if (!validPersonalGlossaryReviewGenerationBinding(decision.generation) || !validPersonalGlossaryReviewMetadataBinding(decision.policy_version))
|
|
132
|
+
return { status: "current_binding_mismatch", reason: "record_binding_mismatch", record: null, reopen_reason: null };
|
|
133
|
+
if (candidate === null || scope === null || validateGlossaryHostClassificationReceipt(input.receipt, candidate.capsule, { candidateProjectionSha256: projection.projection.projection_sha256 }).length > 0 || validateGlossaryAdmissionDecision(decision, candidate.capsule, input.receipt).length > 0)
|
|
134
|
+
return { status: "current_binding_mismatch", reason: "candidate_binding_mismatch", record: null, reopen_reason: null };
|
|
135
|
+
const current = readPersonalGlossaryReviewRecords(options);
|
|
136
|
+
if (current.status === "corrupt" || (current.store !== null && isLegacyReviewStore(current.store)))
|
|
137
|
+
return { status: "records_unavailable", reason: "review_records_unavailable", record: null, reopen_reason: null };
|
|
138
|
+
const records = current.store?.records ?? [];
|
|
139
|
+
const candidateRecord = pendingRecord(decision, scope, queuedAt, null);
|
|
140
|
+
const existing = records.find((record) => record.review_id === candidateRecord.review_id);
|
|
141
|
+
if (existing) {
|
|
142
|
+
if (!isCurrentReviewRecord(existing))
|
|
143
|
+
return { status: "records_unavailable", reason: "review_records_unavailable", record: null, reopen_reason: null };
|
|
144
|
+
if (suppresses(existing))
|
|
145
|
+
return { status: "suppressed", reason: existing.reason, record: existing, reopen_reason: null };
|
|
146
|
+
if (existing.status === "pending") {
|
|
147
|
+
ensurePersonalGlossaryReviewRecordsPrivate(personalGlossaryReviewRecordsPath(options));
|
|
148
|
+
return { status: "unchanged_replay", reason: existing.reason, record: existing, reopen_reason: existing.reopen_reason };
|
|
149
|
+
}
|
|
150
|
+
return { status: "already_terminal", reason: "review_already_terminal", record: null, reopen_reason: null };
|
|
151
|
+
}
|
|
152
|
+
const suppressed = records.filter(isCurrentReviewRecord).find((record) => !terminalReviewRecordExpired(record, queuedAt) && suppresses(record) && sameSuppressionBinding(record, candidateRecord));
|
|
153
|
+
if (suppressed)
|
|
154
|
+
return { status: "suppressed", reason: suppressed.reason, record: suppressed, reopen_reason: null };
|
|
155
|
+
const previous = latestComparableRecord(records, decision.candidate_id, queuedAt);
|
|
156
|
+
const reopenReason = previous ? reopeningReason(previous, candidateRecord) : null;
|
|
157
|
+
const record = reopenReason === null ? candidateRecord : pendingRecord(decision, scope, queuedAt, reopenReason);
|
|
158
|
+
if (records.length >= personalGlossaryReviewRecordsStorageContract().recordsMax)
|
|
159
|
+
return { status: "record_capacity_exceeded", reason: "review_record_capacity_exceeded", record: null, reopen_reason: null };
|
|
160
|
+
privateWritePersonalGlossaryReviewRecords(personalGlossaryReviewRecordsPath(options), `${canonicalGlossaryJson(makePersonalGlossaryReviewStore([...records, record], current.store?.replay_index ?? []))}\n`);
|
|
161
|
+
return { status: reopenReason === null ? "queued" : "reopened", reason: record.reason, record, reopen_reason: reopenReason };
|
|
162
|
+
}
|
|
163
|
+
function currentRecordForDisposition(record, receipt) {
|
|
164
|
+
if (isCurrentReviewRecord(record))
|
|
165
|
+
return record;
|
|
166
|
+
const scope = reviewScope(receipt);
|
|
167
|
+
if (scope === null)
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
return migrateLegacyPendingReviewRecord(record, scope);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Record one signed current-user disposition after current source and replay validation. */
|
|
177
|
+
export function dispositionPersonalGlossaryReviewRecord(input) {
|
|
178
|
+
const now = input.now ?? new Date().toISOString();
|
|
179
|
+
if (!timestamp(now))
|
|
180
|
+
throw new TypeError("review disposition time must be an ISO timestamp");
|
|
181
|
+
if (!digest(input.review_id) || !mapping(input.receipt) || !mapping(input.approval))
|
|
182
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
183
|
+
const options = { env: input.env, platform: input.platform };
|
|
184
|
+
const current = readPersonalGlossaryReviewRecords(options);
|
|
185
|
+
if (current.status !== "current" || current.store === null)
|
|
186
|
+
return { status: current.status === "corrupt" ? "records_unavailable" : "review_not_found", record: null, publication_authorization: null };
|
|
187
|
+
const storedRecord = current.store.records.find((record) => record.review_id === input.review_id);
|
|
188
|
+
if (!storedRecord || terminalReviewRecordExpired(storedRecord, now))
|
|
189
|
+
return { status: "review_not_found", record: null, publication_authorization: null };
|
|
190
|
+
const decisionResult = decidePersonalGlossaryCandidate(input.receipt, options);
|
|
191
|
+
if (decisionResult.status !== "review_required" || decisionResult.decision === null)
|
|
192
|
+
return { status: "current_binding_mismatch", record: null, publication_authorization: null };
|
|
193
|
+
const decision = decisionResult.decision;
|
|
194
|
+
const projection = readCurrentPersonalGlossaryCandidateProjection(options);
|
|
195
|
+
if (projection.status !== "current" || projection.projection === null || !recordMatchesProjection(storedRecord, projection.projection))
|
|
196
|
+
return { status: "current_binding_mismatch", record: null, publication_authorization: null };
|
|
197
|
+
const candidate = exactCandidate(projection.projection.candidates, decision);
|
|
198
|
+
if (candidate === null || decision.candidate_id !== storedRecord.candidate_id || decision.candidate_revision !== storedRecord.candidate_revision || decision.candidate_capsule_sha256 !== storedRecord.candidate_capsule_sha256 || decision.candidate_projection_sha256 !== storedRecord.candidate_projection_sha256 || decision.host_receipt_sha256 !== storedRecord.host_receipt_sha256 || decision.decision_sha256 !== storedRecord.cli_decision_sha256 || decision.semantic_fingerprint !== storedRecord.semantic_fingerprint || decision.generation !== storedRecord.generation || decision.policy_version !== storedRecord.policy_version || validateGlossaryHostClassificationReceipt(input.receipt, candidate.capsule, { candidateProjectionSha256: projection.projection.projection_sha256 }).length > 0 || validateGlossaryAdmissionDecision(decision, candidate.capsule, input.receipt).length > 0)
|
|
199
|
+
return { status: "current_binding_mismatch", record: null, publication_authorization: null };
|
|
200
|
+
const record = currentRecordForDisposition(storedRecord, input.receipt);
|
|
201
|
+
if (!record)
|
|
202
|
+
return { status: "current_binding_mismatch", record: null, publication_authorization: null };
|
|
203
|
+
const trustedHost = readPersonalGlossaryTrustedLocalHost(options);
|
|
204
|
+
if (!trustedHost)
|
|
205
|
+
return { status: "approval_unavailable", record: null, publication_authorization: null };
|
|
206
|
+
const replayEntries = isCurrentReviewStore(current.store) ? currentReplayIndex(current.store.replay_index, now) : [];
|
|
207
|
+
const replayNonceKey = typeof input.approval.nonce === "string" ? replayNonceDigest(input.approval.nonce) : undefined;
|
|
208
|
+
const errors = validatePersonalReviewApprovalReceipt(input.approval, {
|
|
209
|
+
currentUserSubject: trustedHost.subject,
|
|
210
|
+
reviewId: record.review_id,
|
|
211
|
+
candidateId: record.candidate_id,
|
|
212
|
+
candidateRevision: record.candidate_revision,
|
|
213
|
+
candidateProjectionSha256: record.candidate_projection_sha256,
|
|
214
|
+
semanticFingerprint: record.semantic_fingerprint,
|
|
215
|
+
generation: record.generation,
|
|
216
|
+
policyVersion: record.policy_version,
|
|
217
|
+
now: new Date(now),
|
|
218
|
+
trustedHostPublicKey: trustedHost.publicKey,
|
|
219
|
+
consumedReceiptDigests: activeReplayDigestMap(replayEntries, now),
|
|
220
|
+
replayNonceKey,
|
|
221
|
+
});
|
|
222
|
+
let replay;
|
|
223
|
+
let receiptDigest;
|
|
224
|
+
let nonceDigest;
|
|
225
|
+
try {
|
|
226
|
+
replay = personalReviewApprovalReplayStatus(input.approval, replayDigestMap(replayEntries), replayNonceKey);
|
|
227
|
+
receiptDigest = personalReviewApprovalReceiptDigest(input.approval);
|
|
228
|
+
nonceDigest = replayNonceDigest(String(input.approval.nonce));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
232
|
+
}
|
|
233
|
+
if (replay === "conflicting_replay")
|
|
234
|
+
return { status: "approval_conflicting_replay", record: null, publication_authorization: null };
|
|
235
|
+
if (errors.length > 0)
|
|
236
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
237
|
+
if (replay === "exact_replay")
|
|
238
|
+
return { status: "unchanged_replay", record, publication_authorization: publicationAuthorization(record) };
|
|
239
|
+
if (record.status === "terminal")
|
|
240
|
+
return { status: "review_not_pending", record: null, publication_authorization: null };
|
|
241
|
+
const disposition = input.approval.disposition;
|
|
242
|
+
if (disposition === "correct" && typeof input.approval.corrected_meaning !== "string")
|
|
243
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
244
|
+
if (replayEntries.length >= personalGlossaryReviewRecordsStorageContract().replayEntriesMax)
|
|
245
|
+
return { status: "replay_capacity_exceeded", record: null, publication_authorization: null };
|
|
246
|
+
let review;
|
|
247
|
+
try {
|
|
248
|
+
review = createGlossaryReviewRecord({
|
|
249
|
+
capsule: candidate.capsule,
|
|
250
|
+
receipt: input.receipt,
|
|
251
|
+
decision,
|
|
252
|
+
disposition,
|
|
253
|
+
corrected_meaning: input.approval.corrected_meaning,
|
|
254
|
+
corrected_scope: input.approval.corrected_scope,
|
|
255
|
+
disposed_at: input.approval.disposed_at,
|
|
256
|
+
expires_at: reviewExpiry(input.approval.disposed_at),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
261
|
+
}
|
|
262
|
+
const terminal = personalReviewDispositionLifecycle(disposition) === "terminal";
|
|
263
|
+
let nextRecord;
|
|
264
|
+
try {
|
|
265
|
+
nextRecord = sealPersonalGlossaryReviewRecord({
|
|
266
|
+
...record,
|
|
267
|
+
scope: disposition === "correct" ? "personal" : record.scope,
|
|
268
|
+
status: terminal ? "terminal" : "pending",
|
|
269
|
+
disposition,
|
|
270
|
+
review_record: review,
|
|
271
|
+
terminal_at: terminal ? review.disposed_at : null,
|
|
272
|
+
expires_at: terminal ? review.expires_at : null,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return { status: "approval_invalid", record: null, publication_authorization: null };
|
|
277
|
+
}
|
|
278
|
+
const replayEntry = { nonce_sha256: nonceDigest, receipt_sha256: receiptDigest, expires_at: input.approval.expires_at };
|
|
279
|
+
const next = makePersonalGlossaryReviewStore(current.store.records.map((item) => item.review_id === record.review_id ? nextRecord : item), [...replayEntries, replayEntry]);
|
|
280
|
+
try {
|
|
281
|
+
privateWritePersonalGlossaryReviewRecords(personalGlossaryReviewRecordsPath(options), `${canonicalGlossaryJson(next)}\n`);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return { status: "records_unavailable", record: null, publication_authorization: null };
|
|
285
|
+
}
|
|
286
|
+
return { status: "disposed", record: nextRecord, publication_authorization: publicationAuthorization(nextRecord) };
|
|
287
|
+
}
|
|
288
|
+
/** Resolve one stored accept/correct authorization without treating it as current evidence. */
|
|
289
|
+
export function personalGlossaryReviewPublicationAuthorization(input) {
|
|
290
|
+
const now = input.now ?? new Date().toISOString();
|
|
291
|
+
if (!timestamp(now) || !digest(input.review_id) || !digest(input.review_record_sha256))
|
|
292
|
+
return { status: "binding_mismatch", review: null };
|
|
293
|
+
const current = readPersonalGlossaryReviewRecords({ env: input.env, platform: input.platform });
|
|
294
|
+
if (current.status !== "current" || current.store === null)
|
|
295
|
+
return { status: "unavailable", review: null };
|
|
296
|
+
const projection = readCurrentPersonalGlossaryCandidateProjection({ env: input.env, platform: input.platform });
|
|
297
|
+
if (projection.status !== "current" || projection.projection === null)
|
|
298
|
+
return { status: "unavailable", review: null };
|
|
299
|
+
const storedRecord = current.store.records.find((record) => record.review_id === input.review_id);
|
|
300
|
+
if (!storedRecord || !isCurrentReviewRecord(storedRecord) || terminalReviewRecordExpired(storedRecord, now) || !storedRecord.review_record || storedRecord.status !== "terminal")
|
|
301
|
+
return { status: "not_publishable", review: null };
|
|
302
|
+
if (storedRecord.review_record.record_sha256 !== input.review_record_sha256 || !["accept", "correct"].includes(String(storedRecord.disposition)) || !recordMatchesProjection(storedRecord, projection.projection) || storedRecord.candidate_projection_sha256 !== input.candidate_projection_sha256 || storedRecord.candidate_id !== input.capsule.candidate_id || storedRecord.candidate_revision !== input.capsule.candidate_revision || storedRecord.candidate_capsule_sha256 !== input.capsule.capsule_sha256 || storedRecord.host_receipt_sha256 !== input.receipt.receipt_sha256 || storedRecord.cli_decision_sha256 !== input.decision.decision_sha256 || storedRecord.semantic_fingerprint !== input.receipt.semantic_fingerprint || storedRecord.generation !== input.capsule.generation || storedRecord.policy_version !== input.capsule.policy_version || validateGlossaryReviewRecord(storedRecord.review_record, input.capsule, input.receipt, input.decision).length > 0)
|
|
303
|
+
return { status: "binding_mismatch", review: null };
|
|
304
|
+
return { status: "authorized", review: storedRecord.review_record };
|
|
305
|
+
}
|
|
306
|
+
/** Remove expired terminal metadata and receipt replay digests, or a purged local store only. */
|
|
307
|
+
export function maintainPersonalGlossaryReviewRecords(input) {
|
|
308
|
+
if (!timestamp(input.now))
|
|
309
|
+
throw new TypeError("review maintenance time must be an ISO timestamp");
|
|
310
|
+
const options = { env: input.env, platform: input.platform };
|
|
311
|
+
const current = readPersonalGlossaryReviewRecords(options);
|
|
312
|
+
if (current.status === "missing" || current.status === "corrupt")
|
|
313
|
+
return { status: current.status, expired_records: 0, expired_receipts: 0 };
|
|
314
|
+
const pathname = personalGlossaryReviewRecordsPath(options);
|
|
315
|
+
if (input.current_user_purge_authorized === true) {
|
|
316
|
+
fs.rmSync(pathname, { force: true });
|
|
317
|
+
return { status: "purged", expired_records: 0, expired_receipts: 0 };
|
|
318
|
+
}
|
|
319
|
+
if (isLegacyReviewStore(current.store))
|
|
320
|
+
return { status: "unchanged", expired_records: 0, expired_receipts: 0 };
|
|
321
|
+
const records = current.store.records.filter((record) => !terminalReviewRecordExpired(record, input.now));
|
|
322
|
+
const replayIndex = currentReplayIndex(current.store.replay_index, input.now);
|
|
323
|
+
const expiredRecords = current.store.records.length - records.length;
|
|
324
|
+
const expiredReceipts = current.store.replay_index.length - replayIndex.length;
|
|
325
|
+
if (expiredRecords === 0 && expiredReceipts === 0)
|
|
326
|
+
return { status: "unchanged", expired_records: 0, expired_receipts: 0 };
|
|
327
|
+
if (records.length === 0 && replayIndex.length === 0)
|
|
328
|
+
fs.rmSync(pathname, { force: true });
|
|
329
|
+
else
|
|
330
|
+
privateWritePersonalGlossaryReviewRecords(pathname, `${canonicalGlossaryJson(makePersonalGlossaryReviewStore(records, replayIndex))}\n`);
|
|
331
|
+
return { status: "changed", expired_records: expiredRecords, expired_receipts: expiredReceipts };
|
|
332
|
+
}
|
|
@@ -13,7 +13,7 @@ import buildInstructions from "./build/instructions.js";
|
|
|
13
13
|
import optimizeInstructions from "./optimize/instructions.js";
|
|
14
14
|
import auditInstructions from "./audit/instructions.js";
|
|
15
15
|
import documentInstructions from "./document/instructions.js";
|
|
16
|
-
import profileInstructions from "./profile/instructions.js";
|
|
16
|
+
import profileInstructions, { servedInstructions as servedProfileInstructions } from "./profile/instructions.js";
|
|
17
17
|
import { instructions as designInstructions } from "./design/instructions.js";
|
|
18
18
|
import orchestrateInstructions from "./orchestrate/instructions.js";
|
|
19
19
|
import { preCutoverCommand, preCutoverInstructionBody } from "../cli/preCutoverCommand.js";
|
|
@@ -32,6 +32,10 @@ const canonicalInstructions = {
|
|
|
32
32
|
orchestrate: orchestrateInstructions,
|
|
33
33
|
};
|
|
34
34
|
export const CAPABILITY_INSTRUCTIONS = Object.fromEntries(Object.entries(canonicalInstructions).map(([capability, body]) => [capability, preCutoverInstructionBody(body)]));
|
|
35
|
+
Object.defineProperty(CAPABILITY_INSTRUCTIONS, "profile", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: () => preCutoverInstructionBody(servedProfileInstructions()),
|
|
38
|
+
});
|
|
35
39
|
export function capabilityInstructionModulePath(capability) {
|
|
36
40
|
return `packages/cli/src/capabilities/${capability}/instructions.ts`;
|
|
37
41
|
}
|
|
@@ -1,30 +1,57 @@
|
|
|
1
1
|
import { personalGlossaryOutputContract } from "../../registries/glossaryEntryContract.js";
|
|
2
|
+
import { personalGlossaryCandidateProjectionContract } from "../../registries/glossaryCandidateProjectionContract.js";
|
|
3
|
+
import { personalGlossaryCandidateDecisionContract } from "../../registries/glossaryCandidateDecisionContract.js";
|
|
4
|
+
import { personalGlossaryProfileFullContract } from "../../registries/glossaryProfileFullContract.js";
|
|
5
|
+
import { personalGlossaryReviewRecordsContract } from "../../registries/glossaryReviewRecordsContract.js";
|
|
2
6
|
// Capability instructions for profile
|
|
3
7
|
// Served via `agentera prime --context profile --format json`. RFC 2119 modal vocab.
|
|
4
8
|
export const instructions = JSON.parse(String.raw `"# PROFILE\n\n**Persona Reconstruction: Observable Footprint Indexing Logic. Extract, Reconcile, Formalize**\n\nGlyph: ♾ (protocol ref: SG9).\n\nMine the user's session history and produce a structured decision profile for predicting \"What would this person decide?\" Each entry carries numeric confidence, permanence classification, and temporal metadata enabling dormancy decay. The profile is consumed by all capabilities via \u0060status.profile\u0060.\n\n---\n\n## State artifacts\n\nProfile reads bounded signal tiers and decisions; writes PROFILE.md. Artifact path resolution is owned by SKILL.md.\n\n| Artifact | Role | Path |\n|----------|------|------|\n| \u0060profile\u0060 | produces_and_consumes | \u0060profile_context.profile.path\u0060 |\n| \u0060decisions\u0060 | consumes | deferred \u0060capability_context.startup.availability\u0060 row\u0027s \u0060detail_command\u0060 |\n\n**Startup contract**: trust \u0060profile_context\u0060, \u0060capability_context.startup\u0060, and \u0060raw_artifact_read_policy\u0060 from \u0060agentera prime --context profile --format json\u0060. Observe \u0060capability_context.startup.outcome\u0060. Treat \u0060capability_context.startup.availability\u0060 as the inventory: use the bounded context summary for an \u0060included\u0060 family, and run a \u0060deferred\u0060 row\u0027s \u0060detail_command\u0060 before any raw artifact read. Do not manually locate schemas or defensively raw-read state the CLI already serves. Visual-token families (VT/SI/EX/SG/PH) by \u0060skills/agentera/protocol.yaml\u0060.\n\n### Profile format\n\nOutput constraint: ≤30 words per signal, ≤15 words per evidence line.\n\n\u0060\u0060\u0060markdown\n# Decision Profile: [User Name]\n\n<!-- Generated: {date} | Data: {date range from earliest to latest timestamp} | Validated: — -->\n<!-- Sources: {N} memory files, {N} history prompts, {N} conversation exchanges, {N} configs -->\n<!-- Decay parameters: stable λ=0.001, durable λ=0.005, situational λ=0.015 -->\n<!-- Formula: effective_conf = conf × e^(-λ × days_since_confirmed), floor 20 -->\n<!-- Regenerate with /agentera profile -->\n\n## How to Use This Profile\n\nThis profile captures decision-making patterns extracted from {N} months of sessions across {N} projects. Each entry carries inline metadata:\n\n\u0060━ conf:75 | perm:durable | first:2026-01-15 | confirmed:2026-03-28 | challenged:—\u0060\n\n- **conf** (0-100): Evidence-based confidence. 90+ shipped consistently (CS1), 70-89\n established (CS2), 50-69 emerging (CS3), 30-49 single signal (CS4), 0-29 speculative (CS5).\n Line weight tokens: ━ (VT9) high (90-100), ─ (VT10) medium (50-89), ┄ (VT11) low (0-49).\n- **perm**: How stable the decision domain is. stable (decade), durable (year),\n situational (month).\n- **dates**: When the decision was first observed, refreshed,\n and last challenged.\n- **Generated / Validated**: Full rebuild date vs last validate-mode\n maintenance date in the header comment. \u0060agentera prime\u0060 treats the\n profile as fresh through the newer of the two.\n\nWhen consuming this profile, compute effective confidence using the decay formula.\nStale situational entries carry less weight than fresh stable ones.\n\n**When the profile is silent**: If a situation isn't covered, look for the closest trade-off\nheuristic or meta-decision pattern. When truly uncertain, ask.\n\n## Decision-Making Philosophy\n\n[2-3 paragraphs describing the meta-patterns: how this person approaches decisions, what\nframeworks they use, their risk posture, when they decide quickly vs deliberate, what\ninformation they seek before deciding]\n\n## [Category Name]\n\n### [Decision Name]\n\u0060━ conf:75 | perm:durable | first:2026-01-15 | confirmed:2026-03-28 | challenged:—\u0060\n\n- ▸ **Rule**: [Imperative statement an agent can follow directly]\n- ▸ **When**: [Specific conditions or triggers for this rule]\n- ▸ **Why**: [The reasoning, the value or concern that drives this]\n- ▸ **Exceptions**: [Known cases where this was overridden, or \"None observed\"]\n\n[Repeat for each decision in the category. Order by confidence (highest first).]\n\n[Repeat for all 12 categories. Skip categories with no signal.]\n\n## Tensions\n\nEach entry records a contradiction or divergence found during profile generation or challenged during validation. Default status is **unresolved**. Resist the urge to wrap tensions in resolution narratives. Some tensions are real and persistent.\n\n### YYYY-MM-DD: [Short description]\n\n**Decision affected**: [which decision was contradicted]\n**What happened**: [what was observed or said that didn't fit]\n**Status**: unresolved\n\u0060\u0060\u0060\n\n**Writing guidelines**:\n\n- Write rules as imperatives (\"Use X\" not \"[Name] prefers X\")\n- Be specific (\"when building Go CLIs\" not \"when building things\")\n- Always include the *why* because agents need reasoning for edge cases\n- Don't duplicate AGENTS.md. This covers decision *patterns*, not project instructions\n- Omit categories with <2 decisions (insufficient signal)\n- Every entry MUST have inline metadata after the ### heading\n\n---\n\n## Workflow phases\n\nMode-split shape. Two modes: **Full** (regenerate from scratch) and **Validate** (quick incremental check).\n\nCheck if \u0060profile_context.profile.path\u0060 resolves to an existing file. If it does NOT exist, proceed directly to Full mode. If it DOES exist, present the mode choice:\n\n> **Full**: Regenerate from scratch using all session data. Replaces the existing profile including any accumulated tensions. Best when the profile feels significantly outdated or you want a clean baseline.\n>\n> **Validate**: Quick check of your existing profile (~2 minutes). Reviews the entries most worth validating: confirm, challenge, or skip each one. Best for regular maintenance between full regenerations.\n\n### Full Mode\n\nStep markers: display \u0060── step N/4: verb\u0060 before each step.\nSteps: extract, read, synthesize, generate.\n\n#### Step 1: Coverage and extraction\n\nRun the active-runtime Coverage Audit as the first user-visible output of every Full-mode run, then publish bounded tiers:\n\n\u0060\u0060\u0060bash\nagentera report refresh --consent local-history\n\u0060\u0060\u0060\n\nThe refresh runs the coverage audit first and exits flagged (EX2) if available runtimes are skipped without \u0060--accept-coverage-gap\u0060. On success it publishes bounded evidence tiers (full-evidence shards + a bounded signal tier with evidence anchors) as the canonical bounded output. The monolithic corpus is no longer written; tiers are the synthesis input.\n\nThe active runtime IDs are exactly \u0060opencode\u0060, \u0060codex\u0060, \u0060cursor\u0060, and \u0060copilot\u0060. Cursor Agent CLI storage is a Cursor source product, never a separate runtime identity. Apply \u0060--no-codex\u0060, \u0060--no-opencode\u0060, \u0060--no-copilot\u0060, or \u0060--no-cursor\u0060 only when the user selects a partial active-runtime corpus; available skipped sources require \u0060--accept-coverage-gap\u0060.\n\nClaude Code is not a supported runtime. Its transcript parser is available only as an explicit historical importer:\n\n\u0060\u0060\u0060bash\nagentera report refresh --consent local-history --import-source claude\n\u0060\u0060\u0060\n\nBefore that opt-in, warn that transcripts can contain secrets, file contents, and command output. The import is local and read-only. Every imported record is labeled \u0060source_class=historical_import\u0060, \u0060source_product=claude-code\u0060, and \u0060active_runtime=false\u0060; default active analytics exclude it. Never describe imported records as Claude support, health, installation, or active-runtime coverage. Use \u0060agentera report --sources all\u0060 only when the user explicitly asks for historical/all-source analysis, and keep provenance visible.\n\nThe extractor writes instruction documents, history prompts, conversation turns, tool calls, and project config signals as bounded signal records. Read \u0060profile_context.profile.bounded_signals\u0060 to confirm the tier state, signal count, and sufficiency assessment without loading full evidence. If an active source fails, proceed with bounded degradation evidence; if historical import fails, report only the importer failure and do not turn it into runtime health.\n#### Step 2: Read bounded signals\n\nRead the bounded signal tier published in Step 1. The \u0060profile_context.profile.bounded_signals\u0060 status provides \u0060tiers_dir\u0060, \u0060signal_path\u0060, \u0060signal_count\u0060, \u0060profile_signal_count\u0060, and the \u0060sufficiency\u0060 assessment. The signal tier carries only the contract-required fields plus \u0060evidence_anchor\u0060 for every record — no transcript text, raw tool arguments, or raw store paths. Group signals by \u0060signal_type\u0060 for synthesis:\n\n1. **instruction**: Memory files, AGENTS.md (highest signal: explicit user instructions)\n2. **decision** and **correction**: Decision-rich and revision signals from session history\n3. **question**: Interrogative signals surfacing uncertainty and information-gathering intent\n4. **configuration**: Recurring config patterns across projects (most objective: what shipped)\n\nEach signal carries \u0060source_kind\u0060, \u0060signal_type\u0060, \u0060timestamp\u0060, \u0060project_id\u0060, \u0060runtime\u0060, \u0060source_product\u0060, and \u0060evidence_anchor\u0060. The \u0060evidence_anchor\u0060 resolves to exactly one retained full-evidence shard — do not re-scan unbounded history to retrieve a full record.\n\n**Sufficiency gate**: Before synthesizing, check \u0060bounded_signals.sufficiency\u0060. If \u0060sufficient\u0060 is false, surface the underrepresented families with their retained and intended counts. Cap confidence for affected categories to the single-signal band (CS4, 30-49) or below — never fabricate high-confidence claims from sparse or missing evidence. Report the limitation in the profile header and emit \u0060♾ profile · flagged\u0060 (EX2).\n\n**Legacy state**: If \u0060bounded_signals.state\u0060 is \u0060legacy\u0060 (tiers not published, monolithic corpus exists) or \u0060missing\u0060, do not load the corpus whole. Run Step 1 refresh to publish bounded tiers first.\n\n#### Step 3: Categorize and synthesize\n\nGroup signals into 12 categories:\n\n1. **Architecture & Design Patterns**: package layout, abstraction boundaries, API design\n2. **Technology & Tooling Selection**: languages, frameworks, libraries, build tools\n3. **Agent & Automation Philosophy**: agent behavior, autonomy, interaction patterns\n4. **Code Quality & Standards**: error handling, testing, validation, naming\n5. **DX & Project Structure**: directory layout, build targets, configuration\n6. **Scoping & Prioritization**: what to build, milestones, complexity budgets\n7. **Communication Style**: writing preferences, documentation voice\n8. **Process & Workflow**: git workflow, commit conventions, release process\n9. **UI/UX Preferences**: visual patterns, interaction design, CLI vs TUI vs web\n10. **Trade-off Heuristics**: simplicity vs flexibility, speed vs correctness\n11. **Anti-patterns & Rejections**: things actively avoided, with reasoning\n12. **Meta-decision Style**: frameworks used, information gathering, decide vs defer\n\nPer category: identify distinct decisions (not just preferences; decisions have conditions and reasoning), look for the *why*, note exceptions where the rule was overridden.\n\n**Assign confidence** (numeric, 0-100, protocol ref: CS1-CS5)\n\nDecision patterns are empirically verifiable via git history and configs:\n\n| Range | Label | Token | Criteria |\n|-------|-------|-------|----------|\n| 90-100 (CS1) | Shipped consistently | ━ (VT9) | Appears in configs/code across 3+ projects, verifiable from artifacts |\n| 70-89 (CS2) | Established | ━ (VT9) | Consistent across sessions, corroborated by behavior |\n| 50-69 (CS3) | Emerging | ─ (VT10) | Observed multiple times but limited context or minor variations |\n| 30-49 (CS4) | Single signal | ┄ (VT11) | One data point or inferred from adjacent patterns |\n| 0-29 (CS5) | Speculative | ┄ (VT11) | No direct evidence, extrapolated from related decisions |\n\n**Bias check**: Confidence is earned through evidence, not assigned by how insightful the decision sounds. A pithy design principle observed once is 30, not 75.\n\n**Assign permanence class**\n\nPermanence captures domain *stability*, independent of confidence. You can be highly confident about something that will change (85, situational) or uncertain about something deep (35, stable).\n\n| Class | Domain | Timescale |\n|-------|--------|-----------|\n| **stable** | Architecture principles, design patterns, meta-decision heuristics | Decade |\n| **durable** | Tooling choices, code standards, process conventions, DX preferences | Year |\n| **situational** | Current project priorities, active initiative choices, recent tech stack picks | Month |\n\nDefault permanence mapping by category:\n\n- Architecture & Design Patterns, Meta-decision Style → stable\n- Technology & Tooling, Code Quality & Standards, Process & Workflow, DX & Project Structure, Communication Style, Trade-off Heuristics, Anti-patterns → durable\n- Scoping & Prioritization, UI/UX Preferences → situational (unless clearly long-standing)\n- Agent & Automation Philosophy → durable (unless project-specific)\n\nOverride the default when the evidence suggests otherwise.\n\n**Set dates**\n\n- **first**: Earliest timestamp from the source data that evidences this decision\n- **refresh date**: Set to today's date (the generation date)\n- **challenged**: Set to \u0060—\u0060 (none yet on a fresh profile)\n\n**Identify tensions**\n\nLook for cross-category patterns and contradictions: stated principle vs shipped code, conflicts between categories, \"Exceptions\" suggesting a weaker rule. Record contradictions in the Tensions section rather than smoothing them into a coherent narrative.\n\n**Evidence anchor resolution**\n\nEvery synthesized claim must carry the \u0060evidence_anchor\u0060 (the \u0060source_id\u0060) of the signal it was derived from. To check a claim's source identity, resolve the anchor to its retained full-evidence shard — the manifest maps \u0060source_id\u0060 to the owning shard, so only that shard is loaded. Never re-scan unbounded history. If an anchor does not resolve (stale tier, corrupt shard), the claim cannot be checked: lower its confidence or omit it.\n\n#### Step 4: Generate the profile and validate\n\nWrite the decision profile to \u0060profile_context.profile.path\u0060 following the Profile format template above.\n\nIf a previous version exists: copy to the \u0060history/\u0060 subdirectory alongside PROFILE.md, generate new version, show change summary (added, updated, removed).\n\n**Prediction validation**\n\nPick 5 decision-rich prompts NOT used to create profile entries. For each: predict what the profile would recommend, check against what happened. Report accuracy (e.g., \"4/5\").\n\nBelow 3/5: identify categories needing more signal, note in profile header, and emit \u0060♾ profile · flagged\u0060 (EX2) — the profile is written but signal quality is insufficient.\n\n---\n\n### Validate Mode\n\nQuick incremental check. Step markers: display \u0060── step N/3: verb\u0060 before each step.\nSteps: select, present, apply.\n\n#### Step V1: Select entries\n\nIdentify which entries are most worth checking by reading PROFILE.md directly and prioritizing high-confidence, stale, or tension-heavy entries. If PROFILE.md is missing, fall back to Full mode.\n\n#### Step V2: Present entries for validation\n\nPresent entries one at a time: decision name, rule text, reason surfaced, stored vs effective confidence. Ask: **Confirm**, **Challenge**, or **Skip**.\n\n#### Step V3: Apply updates and write\n\nFor each response:\n\n- **Confirm**: Bump \u0060conf\u0060 by 5 (cap at 95). Update \u0060confirmed\u0060 to today's date.\n- **Challenge**: Soften \u0060conf\u0060 by 10 (floor at 10). Update \u0060challenged\u0060 to today's date.\n Append a tension entry to the \u0060## Tensions\u0060 section:\n\n \u0060\u0060\u0060\n ### {today}: {decision name} challenged during validation\n **Decision affected**: {decision name}\n **What happened**: Challenged by user during validation\n **Status**: unresolved\n \u0060\u0060\u0060\n\n- **Skip**: No changes to this entry.\n\nUpdate the header comment before writing:\n\n- Set \u0060Validated:\u0060 to today's date.\n- Preserve \u0060Generated:\u0060 unchanged (last full rebuild date).\n- If \u0060Validated:\u0060 already exists, replace its date; otherwise append \u0060| Validated: {today}\u0060.\n\nWrite updated PROFILE.md. Report: \"Reviewed {N} entries: {N} accepted, {N} challenged, {N} skipped.\" Mention challenged entries by name.\n\n---\n\n## Safety rails\n\n<critical>\n- MUST NOT fabricate decision patterns. Every profile entry MUST be grounded in observed evidence from bounded signal tiers, memory files, configs, or conversation data.\n- MUST NOT assign confidence higher than the evidence warrants. A single data point is 30-49 (CS4), not 70+, regardless of how insightful the decision sounds. When bounded_signals.sufficiency is insufficient, cap affected categories to CS4 or below.\n- MUST NOT load the monolithic corpus.json whole. Profile synthesis reads the bounded signal tier; full-evidence records are retrieved one shard at a time via evidence_anchor.\n- MUST NOT smooth over contradictions. When evidence conflicts, record tensions rather than forcing a coherent narrative.\n- MUST NOT modify the user's session history, memory files, or config files. Profile reads these sources; it never writes to them.\n- MUST NOT share profile contents with external services or include them in commits.\n</critical>\n\n---\n\n## Exit signals\n\nReport one of these statuses at workflow completion (protocol refs: EX1-EX4).\n\nFormat: emit \u0060♾ profile · <status>\u0060 on its own line, followed by a one-sentence summary. For \u0060flagged\u0060 (EX2), \u0060stuck\u0060 (EX3), and \u0060waiting\u0060 (EX4), add a ▸ (VT15) bullet below the summary identifying what needs attention. The exit marker uses profile's canonical glyph ♾ (SG9, U+267E).\n\n- **complete** (EX1): PROFILE.md was written (Full mode) or updated (Validate mode). Metadata changes were applied, prediction accuracy was assessed (Full mode), and changes were summarized.\n- **flagged** (EX2): Profile generation or validation completed but with data quality issues: available runtime history was skipped without explicit acceptance, extraction failed for one or more sources, prediction accuracy was below 3/5, or significant tensions were found that could not be resolved from available evidence.\n- **stuck** (EX3): Cannot generate or validate a profile because extraction scripts failed entirely or session data sources are unreadable.\n- **waiting** (EX4): The user chose Validate mode but PROFILE.md lacks valid metadata. A Full mode run needs user approval, or the requested mode is ambiguous.\n\n---\n\n## Cross-capability integration\n\n### Consumption contract\n\nAll capabilities consume the profile via \u0060status.profile\u0060 from their prime context. Confidence thresholds: 65+ for strong constraint, below 45 for suggestion. Effective confidence decays over time using the formula and parameters declared in the Profile format template (§2). High effective confidence entries are treated as strong constraints; low as suggestions. Full rules are read from PROFILE.md when needed for detailed reasoning.\n\n### Consumed by\n\n- **build** — confidence-weighted summary table in Orient step; high confidence as strong constraints\n- **optimize** — calibrates experimentation style and trade-off preferences\n- **research** — informs applicability judgments; high confidence constrains recommendations\n- **discuss** — reads profile at start of every deliberation; high-confidence entries acknowledged upfront\n- **audit** — calibrates quality bar and which findings matter for this user\n- **plan** — calibrates planning depth and constraint priorities in Orient step\n\n### Fed by\n\n- **discuss** — \u0060decisions\u0060 artifact is a high-signal extraction source. Prefer \u0060agentera state decisions list --limit 20 --format json\u0060 and preserve returned \u0060missing_fields\u0060, \u0060compacted\u0060, \u0060caveats\u0060, and \u0060satisfaction.review_needed\u0060 pressure rather than raw-reading missing historical context.\n\n### Getting started\n\nFirst profile generation:\n\n\u0060\u0060\u0060\n/agentera profile\n\u0060\u0060\u0060\n\nFull extraction across all sources. Produces PROFILE.md at \u0060profile_context.profile.path\u0060.\n\nRegular validation:\n\n\u0060\u0060\u0060\n/agentera profile validate\n\u0060\u0060\u0060\n\nQuick confidence refresh without full regeneration. Run weekly or per-session.\n\nUsing the profile in other capabilities: automatic — all capabilities read \u0060status.profile\u0060 from their prime context. No manual steps needed; just ensure PROFILE.md exists."`);
|
|
5
|
-
const
|
|
6
|
-
|
|
9
|
+
const fullModeExistingGenerationInstructions = `#### Step 1: Verify existing bounded evidence
|
|
10
|
+
|
|
11
|
+
Profile Full uses an already published bounded signal tier. It MUST NOT run \`report refresh\`, collect local history, or create a generation implicitly. Read \`profile_context.profile.bounded_signals\` before synthesis. If it is missing, legacy, stale, or corrupt, stop before any base write and report its explicit-consent recovery. If it is insufficient, continue only with its declared confidence cap and flagged output. Do not load a monolithic corpus or rescan history.
|
|
12
|
+
|
|
13
|
+
**Separate explicit refresh (outside this Full run):** The active runtime IDs are exactly \`opencode\`, \`codex\`, \`cursor\`, and \`copilot\`. A user who explicitly wants a new generation may run \`npx -y agentera@next report refresh --consent local-history\`, then start a new Full run. Cursor Agent CLI storage is a Cursor source product, never a separate runtime identity. Claude Code is not a supported runtime. Its transcript parser is an explicit historical importer only: \`npx -y agentera@next report refresh --consent local-history --import-source claude\`. Before that opt-in, warn that transcripts can contain secrets, file contents, and command output. Every imported record is labeled \`source_class=historical_import\`, \`source_product=claude-code\`, and \`active_runtime=false\`; default active analytics exclude it.
|
|
14
|
+
|
|
15
|
+
#### Step 2: Read bounded signals`;
|
|
16
|
+
/** Build and validate glossary-owned Profile Full instructions only when Profile is requested. */
|
|
17
|
+
export function servedInstructions() {
|
|
18
|
+
const profileGlossaryOutput = personalGlossaryOutputContract();
|
|
19
|
+
const profileGlossaryCandidates = personalGlossaryCandidateProjectionContract();
|
|
20
|
+
const profileGlossaryDecision = personalGlossaryCandidateDecisionContract();
|
|
21
|
+
const profileGlossaryReviews = personalGlossaryReviewRecordsContract();
|
|
22
|
+
const profileGlossaryProfileFull = personalGlossaryProfileFullContract();
|
|
23
|
+
const personalGlossaryInstructions = `#### Step 4: Generate the profile and validate
|
|
7
24
|
|
|
8
25
|
Generate the complete non-glossary decision profile in memory as byte string \`B\`, following the Profile format template above. The generated base MUST NOT contain either personal-glossary marker or a line whose complete heading is \`## Glossary\`.
|
|
9
26
|
|
|
10
|
-
Use this ordered
|
|
27
|
+
Use this ordered base-profile sequence. Do not reorder or combine its writes:
|
|
11
28
|
|
|
12
29
|
<!-- agentera:profile-full-action:capture-owned-glossary -->
|
|
13
30
|
1. **Capture and validate the existing owned Glossary section.** Before copying history or writing any generated bytes, read the existing \`profile_context.profile.path\` only if it exists. Count the literal start marker, literal end marker, and line-anchored \`## Glossary\` heading. If all counts are zero, set the captured section \`G\` to absent. Otherwise require exactly one of each, the start before the end, the heading inside that range, and the complete range to match the deterministic representation below. Its JSON must parse to a mapping with exactly \`schema_version\`, \`as_of\`, \`confidence_basis\`, and \`entries\`; require schema \`${profileGlossaryOutput.sectionSchemaVersion}\`, a valid calendar date, one integer confidence basis per unique case-insensitive term, and shared-primitive-valid entries. Reject unmatched markers, duplicate sections or terms, an unowned heading, malformed JSON or fields, or any other ambiguous boundary before a history copy or base write. Capture \`G\` as the exact bytes from the first byte of the start marker through the final byte of the end marker, inclusive.
|
|
14
31
|
<!-- agentera:profile-full-action:write-base-profile -->
|
|
15
|
-
2. **Write the base profile
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
2. **Write the base profile while preserving the owned section.** If a previous profile exists and validation succeeded, first copy its original bytes to the \`history/\` subdirectory alongside PROFILE.md. If \`G\` is absent, write \`B\` exactly to \`profile_context.profile.path\`. If \`G\` is present, write exactly \`B + (B ends with "\\n" ? "\\n" : "\\n\\n") + G + "\\n"\`. This replaces all generated non-glossary content while carrying the validated owned range byte-for-byte; do not parse, re-render, trim, or normalize \`G\`. The base profile now exists even on first generation.
|
|
33
|
+
|
|
34
|
+
<!-- agentera:profile-full-action:consume-existing-personal-glossary-generation -->
|
|
35
|
+
3. **Consume one existing personal glossary generation.** Only after the base write, run \`${profileGlossaryCandidates.candidateReadCommand} list --limit ${profileGlossaryProfileFull.candidateListLimit} --format json\` once. Bind this Full run to that response's \`generation\`, \`policy_version\`, and \`candidate_projection_sha256\`; do not follow \`next_cursor\`. This command reads the existing consent-bound user-local projection bound to the readable current tier generation. Do not refresh history, acquire consent, collect or reread history, discover candidates, create or persist a projection, or create another generation. If its coverage report is degraded, stop the personal glossary flow after this list: do not call \`get\`, classify, invoke decision or review queue, or publish. Keep \`B\` and \`G\` exactly as written, report the explicit \`npx -y agentera@next report refresh --consent local-history\` recovery, and continue without personal publication. If the current tier generation or projection is missing, stale, corrupt, empty, or the bounded read fails, use the same preservation and recovery.
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
<!-- agentera:profile-full-action:decide-personal-glossary-candidates -->
|
|
38
|
+
4. **Classify only the bounded selection, then use CLI admission.** Only when Step 3 is not degraded, for each returned summary use \`${profileGlossaryCandidates.candidateReadCommand} get\` with every required binding. Classify only that bounded exact view and its safe context. Submit a \`${profileGlossaryDecision.receiptConstructionRequestSchemaVersion}\` request to \`${profileGlossaryDecision.command} --input - --format json\` with exactly \`${profileGlossaryDecision.receiptConstructionRequestFields.join("\`, \`")}\`; use the exact candidate bindings from the list and get results, and supply only the host classification. The CLI computes \`semantic_fingerprint\` and \`receipt_sha256\` and returns the receipt and decision needed by queue or publication. Host classification is semantic evidence, never admission authority. Preserve the CLI outcome and reason exactly: do not synthesize or upgrade a decision. If an exact read or decision fails, report one bounded failure and do not publish that candidate. An inferred or ambiguous result is never eligible for automatic admission. A project-scoped result must abstain; never inspect project state to resolve it.
|
|
39
|
+
|
|
40
|
+
<!-- agentera:profile-full-action:queue-personal-glossary-reviews -->
|
|
41
|
+
5. **Queue review before asking.** For every \`review_required\` result, submit the same receipt to \`${profileGlossaryReviews.command} queue --input - --format json\`. This is the durable review path. If a host question channel is available, show at most ${profileGlossaryProfileFull.questionReviewMaximum} queued review cards, each limited to term, proposed meaning, reason, occurrence count, and safe context when present. A conversational answer is not a disposition or publication approval. It must go through the authenticated local-host review disposition flow. Without a question channel, ask nothing and rely on the durable queue. For \`abstain\`, keep the bounded reason explainable in the final summary and do not queue or publish it.
|
|
42
|
+
|
|
43
|
+
<!-- agentera:profile-full-action:publish-authorized-explicit-candidates -->
|
|
44
|
+
6. **Publish only current explicit automatic decisions.** For each CLI \`automatic_admission\` with reason \`explicit_current_authorized\`, submit the receipt, exact CLI decision, and one injected \`as_of\` date to \`${profileGlossaryOutput.command} --input - --format json\`. Do not use \`--dry-run\` as publication and do not publish a \`review_required\` or \`abstain\` result. The publisher revalidates the current generation and changes only the owned \`G\` range at the same \`profile_context.profile.path\`; a failed or replayed publication cannot replace the base profile or any unapproved glossary entry.
|
|
45
|
+
|
|
46
|
+
Show the non-glossary change summary (added, updated, removed), followed by one compact bounded glossary summary: automatic publications, queued reviews, abstentions, and failures. Do not print candidate pages, history, anchors, project keys, or project glossary state.
|
|
20
47
|
|
|
21
48
|
### Personal Glossary section
|
|
22
49
|
|
|
23
|
-
Profile Full
|
|
50
|
+
Profile Full preserves a validated existing personal Glossary section until the canonical publisher receives a current authorized decision. It never reads a project glossary: do not accept, resolve, import, or inspect a project root, \`.agentera/glossary.yaml\`, project glossary artifact identity, or project-file provenance. Profile performs no consumer lookup, project precedence, or semantic-equivalence review; Discuss, Plan, and Build obtain active consumer behavior separately through \`agentera report glossary-advice\`.
|
|
24
51
|
|
|
25
|
-
Each entry is exactly the shared primitive from \`references/artifacts/glossary-entry-contract.yaml\`: \`term\`, \`meaning\`, \`confidence\`, \`permanence\`, \`temporal\`, and personal \`provenance\`. Do not restate or replace that authority's confidence, permanence, or provenance rules. Decision-pattern confidence and glossary-entry confidence are separate domains; glossary work MUST NOT alter decision headings, metadata, or confidence.
|
|
52
|
+
Each publisher-created entry is exactly the shared primitive from \`references/artifacts/glossary-entry-contract.yaml\`: \`term\`, \`meaning\`, \`confidence\`, \`permanence\`, \`temporal\`, and personal \`provenance\`. Do not restate or replace that authority's confidence, permanence, or provenance rules. Decision-pattern confidence and glossary-entry confidence are separate domains; glossary work MUST NOT alter decision headings, metadata, or confidence.
|
|
26
53
|
|
|
27
|
-
|
|
54
|
+
The separate publisher writes exactly one independently owned section in this deterministic representation, sorted by case-insensitive term identity:
|
|
28
55
|
|
|
29
56
|
~~~markdown
|
|
30
57
|
<!-- agentera:personal-glossary:start -->
|
|
@@ -50,13 +77,21 @@ Write exactly one independently owned section in this deterministic representati
|
|
|
50
77
|
<!-- agentera:personal-glossary:end -->
|
|
51
78
|
~~~
|
|
52
79
|
|
|
53
|
-
The section-level \`confidence_basis\` records confidence at establishment or the latest qualifying refresh so decay is derived from that basis rather than repeatedly compounded from rendered confidence. It is lifecycle metadata, not an entry field.
|
|
80
|
+
The section-level \`confidence_basis\` records confidence at establishment or the latest qualifying refresh so decay is derived from that basis rather than repeatedly compounded from rendered confidence. It is lifecycle metadata, not an entry field. Only the \`${profileGlossaryOutput.requestSchemaVersion}\` publication operation may add, refresh, or decay this section. Profile Full preserves \`G\` exactly until that operation authorizes its replacement.
|
|
54
81
|
|
|
55
|
-
The
|
|
82
|
+
The publisher adds the section when absent and otherwise replaces only the exact owned marker range, preserving every other byte. Unmatched markers, duplicate owned sections, or an unowned \`## Glossary\` heading fail before effects. New explicit entries retain exactly one admitted anchor; inferred entries retain exactly two pairwise-distinct anchors.
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
The publisher merges terms case-insensitively. It preserves established spelling and \`observed_at\`. Matching fresh evidence refreshes confidence, confidence basis, evidence anchors, and \`last_confirmed_at\` only when meaning and provenance kind are unchanged; preserve permanence. Divergent meaning or provenance kind is a conflict, not an overwrite. Without fresh evidence, retain the entry and permanence, and derive integer confidence from the basis using the existing Profile exponential formula, the entry's unchanged permanence-class lambda, elapsed UTC calendar days from \`last_confirmed_at\`, the existing floor, and nearest-integer rounding. Age alone never deletes an entry. Identical input on the same date must produce identical bytes.
|
|
58
85
|
`;
|
|
59
|
-
|
|
86
|
+
const servedInstructionBody = instructions
|
|
87
|
+
.replace("Steps: extract, read, synthesize, generate.", "Steps: verify, read, synthesize, generate.")
|
|
88
|
+
.replace(/#### Step 1: Coverage and extraction[\s\S]*?#### Step 2: Read bounded signals/, fullModeExistingGenerationInstructions)
|
|
89
|
+
.replace("Read the bounded signal tier published in Step 1.", "Read the existing bounded signal tier.")
|
|
90
|
+
.replace("Run Step 1 refresh to publish bounded tiers first.", "Stop and report the explicit refresh recovery; do not refresh during Full mode.")
|
|
91
|
+
.replace("#### Step 4: Generate the profile and validate\n\nWrite the decision profile to `profile_context.profile.path` following the Profile format template above.\n\nIf a previous version exists: copy to the `history/` subdirectory alongside PROFILE.md, generate new version, show change summary (added, updated, removed).", personalGlossaryInstructions)
|
|
92
|
+
.replace("Full extraction across all sources. Produces PROFILE.md at `profile_context.profile.path`.", "Uses existing consent-bound bounded evidence and a current personal glossary generation. Produces PROFILE.md at `profile_context.profile.path`.");
|
|
93
|
+
return servedInstructionBody;
|
|
94
|
+
}
|
|
60
95
|
export function loadProfileDecayParameters() {
|
|
61
96
|
const lambdas = /Decay parameters: stable λ=([0-9.]+), durable λ=([0-9.]+), situational λ=([0-9.]+)/.exec(instructions);
|
|
62
97
|
const floor = /Formula: effective_conf = conf × e\^\(-λ × days_since_confirmed\), floor (\d+)/.exec(instructions);
|
|
@@ -67,4 +102,4 @@ export function loadProfileDecayParameters() {
|
|
|
67
102
|
floor: Number(floor[1]),
|
|
68
103
|
};
|
|
69
104
|
}
|
|
70
|
-
export default
|
|
105
|
+
export default instructions;
|