forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -29,7 +29,10 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
29
29
|
backlog: Object.freeze(['open', 'cancelled']),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// `reclaimable` is the historical row left behind when an expired active lease is
|
|
33
|
+
// superseded. It is distinct from `expired`: both are non-authoritative history,
|
|
34
|
+
// but reclaimable records that the slot was deliberately freed for another owner.
|
|
35
|
+
const CLAIM_STATES = Object.freeze(['active', 'released', 'expired', 'reclaimable']);
|
|
33
36
|
|
|
34
37
|
const DEFAULT_PRIORITY_RANK = 2;
|
|
35
38
|
const MAX_DISPLAY_PRIORITY_RANK = 4;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* global ActiveXObject, Enumerator, VBArray, WScript */
|
|
4
|
+
|
|
5
|
+
// This file is executed by Windows Script Host and loaded by Bun/Node tests.
|
|
6
|
+
// Keep its syntax compatible with the system JScript engine.
|
|
7
|
+
var ADS_PATH_FILE = 1;
|
|
8
|
+
var ADS_SD_FORMAT_IID = 1;
|
|
9
|
+
var ADS_SD_FORMAT_RAW = 2;
|
|
10
|
+
var ADS_SECURITY_INFO_OWNER = 1;
|
|
11
|
+
var ADS_SECURITY_INFO_DACL = 4;
|
|
12
|
+
var PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000;
|
|
13
|
+
var SE_DACL_PRESENT = 0x0004;
|
|
14
|
+
var SE_DACL_PROTECTED = 0x1000;
|
|
15
|
+
var SE_SELF_RELATIVE = 0x8000;
|
|
16
|
+
var FILE_ALL_ACCESS = 0x001f01ff;
|
|
17
|
+
var ACCESS_ALLOWED_ACE_TYPE = 0;
|
|
18
|
+
var OBJECT_INHERIT_ACE = 1;
|
|
19
|
+
var CONTAINER_INHERIT_ACE = 2;
|
|
20
|
+
var MAX_TARGETS = 128;
|
|
21
|
+
|
|
22
|
+
function invariant(condition, message) {
|
|
23
|
+
if (!condition) throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function atStage(code, action) {
|
|
27
|
+
try {
|
|
28
|
+
return action();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
error.forgeExitCode = code;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function bytesFromSafeArray(value) {
|
|
36
|
+
var values = new VBArray(value).toArray();
|
|
37
|
+
var bytes = [];
|
|
38
|
+
for (var i = 0; i < values.length; i++) bytes.push(values[i] & 0xff);
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function u16(bytes, offset) {
|
|
43
|
+
invariant(offset >= 0 && offset + 2 <= bytes.length, 'truncated uint16');
|
|
44
|
+
return bytes[offset] | (bytes[offset + 1] << 8);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function u32(bytes, offset) {
|
|
48
|
+
invariant(offset >= 0 && offset + 4 <= bytes.length, 'truncated uint32');
|
|
49
|
+
return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sidAt(bytes, offset) {
|
|
53
|
+
invariant(offset > 0 && offset + 8 <= bytes.length, 'missing SID');
|
|
54
|
+
var revision = bytes[offset];
|
|
55
|
+
var count = bytes[offset + 1];
|
|
56
|
+
var end = offset + 8 + (count * 4);
|
|
57
|
+
invariant(revision === 1 && end <= bytes.length, 'malformed SID');
|
|
58
|
+
var authority = 0;
|
|
59
|
+
for (var i = 0; i < 6; i++) authority = (authority * 256) + bytes[offset + 2 + i];
|
|
60
|
+
invariant(authority <= 9007199254740991, 'unsupported SID authority');
|
|
61
|
+
var parts = ['S', String(revision), String(authority)];
|
|
62
|
+
for (var j = 0; j < count; j++) parts.push(String(u32(bytes, offset + 8 + (j * 4))));
|
|
63
|
+
return { end: end, value: parts.join('-') };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inspectDescriptor(bytes, expectedOwnerSid, expectedAceFlags) {
|
|
67
|
+
invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
|
|
68
|
+
invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(expectedOwnerSid), 'invalid expected SID');
|
|
69
|
+
invariant(bytes[0] === 1, 'unsupported security descriptor revision');
|
|
70
|
+
var control = u16(bytes, 2);
|
|
71
|
+
invariant((control & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
|
|
72
|
+
invariant((control & SE_DACL_PRESENT) !== 0, 'DACL is absent');
|
|
73
|
+
invariant((control & SE_DACL_PROTECTED) !== 0, 'DACL is not protected');
|
|
74
|
+
var owner = sidAt(bytes, u32(bytes, 4));
|
|
75
|
+
invariant(owner.value === expectedOwnerSid, 'owner changed');
|
|
76
|
+
var daclOffset = u32(bytes, 16);
|
|
77
|
+
invariant(daclOffset > 0 && daclOffset + 8 <= bytes.length, 'DACL is malformed');
|
|
78
|
+
var aclRevision = bytes[daclOffset];
|
|
79
|
+
var aclSize = u16(bytes, daclOffset + 2);
|
|
80
|
+
var aceCount = u16(bytes, daclOffset + 4);
|
|
81
|
+
invariant(aclRevision === 2, 'unsupported ACL revision');
|
|
82
|
+
invariant(aclSize >= 8 && daclOffset + aclSize <= bytes.length, 'ACL size is malformed');
|
|
83
|
+
invariant(aceCount === 1, 'DACL must contain one ACE');
|
|
84
|
+
var aceOffset = daclOffset + 8;
|
|
85
|
+
invariant(aceOffset + 8 <= daclOffset + aclSize, 'ACE is truncated');
|
|
86
|
+
var aceType = bytes[aceOffset];
|
|
87
|
+
var aceFlags = bytes[aceOffset + 1];
|
|
88
|
+
var aceSize = u16(bytes, aceOffset + 2);
|
|
89
|
+
invariant(aceType === ACCESS_ALLOWED_ACE_TYPE, 'ACE must allow access');
|
|
90
|
+
invariant(aceFlags === expectedAceFlags, 'ACE flags are not exact');
|
|
91
|
+
invariant(aceSize >= 16 && aceOffset + aceSize === daclOffset + aclSize, 'ACE size is not exact');
|
|
92
|
+
invariant(u32(bytes, aceOffset + 4) === FILE_ALL_ACCESS, 'ACE is not full control');
|
|
93
|
+
var trustee = sidAt(bytes, aceOffset + 8);
|
|
94
|
+
invariant(trustee.value === expectedOwnerSid, 'ACE trustee is not the owner');
|
|
95
|
+
invariant(trustee.end === aceOffset + aceSize, 'ACE contains trailing data');
|
|
96
|
+
return { aceFlags: aceFlags, ownerSid: owner.value };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inspectIidDescriptor(descriptor, expectedOwner, expectedAceFlags, enumeratorFactory) {
|
|
100
|
+
invariant(descriptor && descriptor.Owner, 'IID owner is absent');
|
|
101
|
+
if (expectedOwner !== null) invariant(String(descriptor.Owner) === expectedOwner, 'IID owner changed');
|
|
102
|
+
invariant((descriptor.Control & SE_DACL_PRESENT) !== 0, 'IID DACL is absent');
|
|
103
|
+
invariant((descriptor.Control & SE_DACL_PROTECTED) !== 0, 'IID DACL is not protected');
|
|
104
|
+
var dacl = descriptor.DiscretionaryAcl;
|
|
105
|
+
invariant(dacl && dacl.AceCount === 1, 'IID DACL must contain one ACE');
|
|
106
|
+
var entries = enumeratorFactory ? enumeratorFactory(dacl) : new Enumerator(dacl);
|
|
107
|
+
invariant(!entries.atEnd(), 'IID ACE is absent');
|
|
108
|
+
var ace = entries.item();
|
|
109
|
+
invariant(ace.AceType === ACCESS_ALLOWED_ACE_TYPE, 'IID ACE must allow access');
|
|
110
|
+
invariant((ace.AccessMask >>> 0) === FILE_ALL_ACCESS, 'IID ACE is not full control');
|
|
111
|
+
invariant(ace.AceFlags === expectedAceFlags, 'IID ACE flags are not exact');
|
|
112
|
+
entries.moveNext();
|
|
113
|
+
invariant(entries.atEnd(), 'IID DACL contains extra ACEs');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rawDescriptor(utility, targetPath) {
|
|
117
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
|
|
118
|
+
return bytesFromSafeArray(utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_RAW));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function iidDescriptor(utility, targetPath) {
|
|
122
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
|
|
123
|
+
return utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_IID);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ownerSid(bytes) {
|
|
127
|
+
invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
|
|
128
|
+
invariant(bytes[0] === 1, 'unsupported security descriptor revision');
|
|
129
|
+
invariant((u16(bytes, 2) & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
|
|
130
|
+
return sidAt(bytes, u32(bytes, 4)).value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readTargets(environment, fso) {
|
|
134
|
+
var countText = String(environment('FORGE_PRIVATE_ACL_COUNT'));
|
|
135
|
+
invariant(/^[1-9][0-9]*$/.test(countText), 'invalid target count');
|
|
136
|
+
var count = Number(countText);
|
|
137
|
+
invariant(count <= MAX_TARGETS, 'too many ACL targets');
|
|
138
|
+
var targets = [];
|
|
139
|
+
for (var i = 0; i < count; i++) {
|
|
140
|
+
var targetPath = String(environment('FORGE_PRIVATE_ACL_TARGET_' + i));
|
|
141
|
+
invariant(targetPath.length > 0 && targetPath.indexOf('\0') === -1, 'invalid ACL target');
|
|
142
|
+
var isDirectory = fso.FolderExists(targetPath);
|
|
143
|
+
var isFile = fso.FileExists(targetPath);
|
|
144
|
+
invariant(isDirectory !== isFile, 'ACL target does not exist');
|
|
145
|
+
targets.push({ isDirectory: isDirectory, path: targetPath });
|
|
146
|
+
}
|
|
147
|
+
return targets;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function applyOwnerOnlyDacl(utility, target, currentSid, createObject, descriptor) {
|
|
151
|
+
var dacl = createObject('AccessControlList');
|
|
152
|
+
dacl.AclRevision = 2;
|
|
153
|
+
var ace = createObject('AccessControlEntry');
|
|
154
|
+
ace.Trustee = currentSid;
|
|
155
|
+
ace.AccessMask = FILE_ALL_ACCESS;
|
|
156
|
+
ace.AceType = ACCESS_ALLOWED_ACE_TYPE;
|
|
157
|
+
ace.AceFlags = target.isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
|
|
158
|
+
dacl.AddAce(ace);
|
|
159
|
+
descriptor.Owner = currentSid;
|
|
160
|
+
descriptor.DiscretionaryAcl = dacl;
|
|
161
|
+
descriptor.Control = descriptor.Control | SE_DACL_PRESENT | SE_DACL_PROTECTED;
|
|
162
|
+
utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL | PROTECTED_DACL_SECURITY_INFORMATION;
|
|
163
|
+
utility.SetSecurityDescriptor(target.path, ADS_PATH_FILE, descriptor, ADS_SD_FORMAT_IID);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function runWithDependencies(dependencies) {
|
|
167
|
+
var environment = dependencies.environment;
|
|
168
|
+
var fso = dependencies.fso;
|
|
169
|
+
var utility = dependencies.utility;
|
|
170
|
+
var createObject = dependencies.createObject;
|
|
171
|
+
var enumeratorFactory = dependencies.enumeratorFactory;
|
|
172
|
+
var currentSid = String(environment('FORGE_PRIVATE_ACL_SID'));
|
|
173
|
+
invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(currentSid), 'invalid current SID');
|
|
174
|
+
var targets = readTargets(environment, fso);
|
|
175
|
+
var owners = [];
|
|
176
|
+
var iidOwners = [];
|
|
177
|
+
var i;
|
|
178
|
+
|
|
179
|
+
// Prove every target and descriptor are readable before the first mutation.
|
|
180
|
+
for (i = 0; i < targets.length; i++) {
|
|
181
|
+
var beforeRaw = atStage(20, function () { return rawDescriptor(utility, targets[i].path); });
|
|
182
|
+
var beforeIid = atStage(21, function () { return iidDescriptor(utility, targets[i].path); });
|
|
183
|
+
owners.push(atStage(22, function () { return ownerSid(beforeRaw); }));
|
|
184
|
+
iidOwners.push(atStage(23, function () {
|
|
185
|
+
invariant(beforeIid && beforeIid.Owner, 'precheck IID owner is absent');
|
|
186
|
+
return String(beforeIid.Owner);
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (i = 0; i < targets.length; i++) {
|
|
191
|
+
// Recheck immediately before changing the owner and DACL together.
|
|
192
|
+
var currentIid = atStage(30, function () {
|
|
193
|
+
var isDirectory = fso.FolderExists(targets[i].path);
|
|
194
|
+
var isFile = fso.FileExists(targets[i].path);
|
|
195
|
+
invariant(isDirectory === targets[i].isDirectory && isFile !== isDirectory, 'target type changed before mutation');
|
|
196
|
+
invariant(ownerSid(rawDescriptor(utility, targets[i].path)) === owners[i], 'owner changed before mutation');
|
|
197
|
+
var descriptor = iidDescriptor(utility, targets[i].path);
|
|
198
|
+
invariant(descriptor && descriptor.Owner, 'IID owner is absent before mutation');
|
|
199
|
+
invariant(String(descriptor.Owner) === iidOwners[i], 'IID owner changed before mutation');
|
|
200
|
+
return descriptor;
|
|
201
|
+
});
|
|
202
|
+
atStage(31, function () { applyOwnerOnlyDacl(utility, targets[i], currentSid, createObject, currentIid); });
|
|
203
|
+
var expectedFlags = targets[i].isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
|
|
204
|
+
atStage(32, function () {
|
|
205
|
+
inspectDescriptor(rawDescriptor(utility, targets[i].path), currentSid, expectedFlags);
|
|
206
|
+
});
|
|
207
|
+
atStage(33, function () {
|
|
208
|
+
inspectIidDescriptor(iidDescriptor(utility, targets[i].path), null, expectedFlags, enumeratorFactory);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function main() {
|
|
215
|
+
try {
|
|
216
|
+
var shell = new ActiveXObject('WScript.Shell');
|
|
217
|
+
runWithDependencies({
|
|
218
|
+
createObject: function (name) { return new ActiveXObject(name); },
|
|
219
|
+
environment: shell.Environment('PROCESS'),
|
|
220
|
+
fso: new ActiveXObject('Scripting.FileSystemObject'),
|
|
221
|
+
utility: new ActiveXObject('ADsSecurityUtility')
|
|
222
|
+
});
|
|
223
|
+
WScript.Quit(0);
|
|
224
|
+
} catch (_error) {
|
|
225
|
+
var exitCode = _error && typeof _error.forgeExitCode === 'number' ? _error.forgeExitCode : 1;
|
|
226
|
+
WScript.Quit(exitCode);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
231
|
+
module.exports = {
|
|
232
|
+
FILE_ALL_ACCESS: FILE_ALL_ACCESS,
|
|
233
|
+
inspectDescriptor: inspectDescriptor,
|
|
234
|
+
inspectIidDescriptor: inspectIidDescriptor,
|
|
235
|
+
runWithDependencies: runWithDependencies
|
|
236
|
+
};
|
|
237
|
+
} else {
|
|
238
|
+
main();
|
|
239
|
+
}
|
package/lib/lefthook-wiring.js
CHANGED
|
@@ -55,6 +55,20 @@ pre-push:
|
|
|
55
55
|
run: npm test --if-present
|
|
56
56
|
`;
|
|
57
57
|
|
|
58
|
+
// The same config MINUS the TDD pre-commit job, written when the project already has its own
|
|
59
|
+
// pre-commit TDD/coupling gate (kernel 5b425a85 / 2699b234). Stacking a second gate on the same
|
|
60
|
+
// commit is the reported bug, so Forge defers on pre-commit but still wires pre-push.
|
|
61
|
+
const FORGE_USER_LEFTHOOK_YML_NO_TDD = `# Forge git hooks — installed by \`forge setup\` / \`forge init\`.
|
|
62
|
+
# Your project already has its own pre-commit TDD/coupling gate, so Forge did NOT add a second
|
|
63
|
+
# one here. To use Forge's gate instead, remove yours and run: forge gate enable rail.tdd_intent
|
|
64
|
+
# Edit freely to add your own checks — Forge only replaces a fully-commented stub.
|
|
65
|
+
|
|
66
|
+
pre-push:
|
|
67
|
+
commands:
|
|
68
|
+
tests:
|
|
69
|
+
run: npm test --if-present
|
|
70
|
+
`;
|
|
71
|
+
|
|
58
72
|
/**
|
|
59
73
|
* A fresh `lefthook install` (run by lefthook's own npm postinstall) drops a stock
|
|
60
74
|
* EXAMPLE lefthook.yml with every hook commented out. That disposable stub used to
|
|
@@ -221,10 +235,13 @@ npm test --if-present || exit 1
|
|
|
221
235
|
* `skipped`) rather than destroyed.
|
|
222
236
|
*
|
|
223
237
|
* @param {string} projectRoot - Absolute path to the project root.
|
|
238
|
+
* @param {{ skipHooks?: string[] }} [options] - Hook names to leave alone entirely. `forge setup`
|
|
239
|
+
* passes `['pre-commit']` when the project already has its own pre-commit TDD/coupling gate,
|
|
240
|
+
* so Forge defers instead of stacking a second one (kernel 5b425a85 / 2699b234).
|
|
224
241
|
* @returns {{ installed: boolean, method?: string, hooksDir?: string,
|
|
225
242
|
* written?: string[], skipped?: string[], reason?: string }}
|
|
226
243
|
*/
|
|
227
|
-
function installNativeGitHooks(projectRoot) {
|
|
244
|
+
function installNativeGitHooks(projectRoot, options = {}) {
|
|
228
245
|
const hooksDir = resolveGitHooksDir(projectRoot);
|
|
229
246
|
if (!hooksDir) {
|
|
230
247
|
return { installed: false, reason: 'not-a-git-repo' };
|
|
@@ -235,9 +252,11 @@ function installNativeGitHooks(projectRoot) {
|
|
|
235
252
|
return { installed: false, reason: `hooks-dir-unwritable: ${error.message}` };
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
const skipHooks = new Set(Array.isArray(options.skipHooks) ? options.skipHooks : []);
|
|
238
256
|
const written = [];
|
|
239
257
|
const skipped = [];
|
|
240
258
|
for (const [name, body] of Object.entries(NATIVE_HOOK_BODIES)) {
|
|
259
|
+
if (skipHooks.has(name)) continue;
|
|
241
260
|
const outcome = writeNativeHook(path.join(hooksDir, name), body);
|
|
242
261
|
(outcome === 'written' ? written : skipped).push(name);
|
|
243
262
|
}
|
|
@@ -406,6 +425,7 @@ function verifyHooksActive(projectRoot) {
|
|
|
406
425
|
module.exports = {
|
|
407
426
|
FORGE_NATIVE_HOOK_SENTINEL,
|
|
408
427
|
FORGE_USER_LEFTHOOK_YML,
|
|
428
|
+
FORGE_USER_LEFTHOOK_YML_NO_TDD,
|
|
409
429
|
forgeShouldWriteLefthookConfig,
|
|
410
430
|
resolveGitHooksDir,
|
|
411
431
|
installNativeGitHooks,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const MEMORY_REVIEW_ENTRY_LIMIT = 200;
|
|
6
|
+
const DEFAULT_FINDING_LIMIT = 50;
|
|
7
|
+
const STALE_AFTER_DAYS = 90;
|
|
8
|
+
const STALE_AFTER_MS = STALE_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
|
9
|
+
const VOLATILE_FIELDS = new Set([
|
|
10
|
+
'id', 'key', 'memory_id', 'created_at', 'updated_at', 'timestamp', 'revision',
|
|
11
|
+
'createdAt', 'updatedAt',
|
|
12
|
+
]);
|
|
13
|
+
const NEGATION_PREFIXES = Object.freeze(['do not ', "don't ", 'never ']);
|
|
14
|
+
|
|
15
|
+
function compareCodeUnits(left, right) {
|
|
16
|
+
if (left < right) return -1;
|
|
17
|
+
if (left > right) return 1;
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeText(value) {
|
|
22
|
+
return String(value)
|
|
23
|
+
.normalize('NFKC')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[’']/g, "'")
|
|
26
|
+
.replace(/[^\p{L}\p{N}'\s]/gu, ' ')
|
|
27
|
+
.replace(/\s+/g, ' ')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canonicalize(value) {
|
|
32
|
+
if (typeof value === 'string') return normalizeText(value);
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map(canonicalize).sort((left, right) =>
|
|
35
|
+
compareCodeUnits(JSON.stringify(left), JSON.stringify(right)));
|
|
36
|
+
}
|
|
37
|
+
if (!value || typeof value !== 'object') return value;
|
|
38
|
+
return Object.fromEntries(Object.keys(value)
|
|
39
|
+
.filter(key => !VOLATILE_FIELDS.has(key))
|
|
40
|
+
.sort(compareCodeUnits)
|
|
41
|
+
.map(key => [key, canonicalize(value[key])]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function payloadFor(entry) {
|
|
45
|
+
if (entry && Object.hasOwn(entry, 'note')) return entry.note;
|
|
46
|
+
if (entry && Object.hasOwn(entry, 'value')) return entry.value;
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fingerprint(value) {
|
|
51
|
+
return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function reviewId(kind, parts) {
|
|
55
|
+
const digest = createHash('sha256')
|
|
56
|
+
.update(`${kind}\0${[...parts].sort(compareCodeUnits).join('\0')}`)
|
|
57
|
+
.digest('hex')
|
|
58
|
+
.slice(0, 16);
|
|
59
|
+
return `memory-${kind}-${digest}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function claimFor(entry) {
|
|
63
|
+
const payload = payloadFor(entry);
|
|
64
|
+
if (typeof payload !== 'string') return null;
|
|
65
|
+
let text = normalizeText(payload);
|
|
66
|
+
let negative = false;
|
|
67
|
+
const prefix = NEGATION_PREFIXES.find(candidate => text.startsWith(candidate));
|
|
68
|
+
if (prefix) {
|
|
69
|
+
negative = true;
|
|
70
|
+
text = text.slice(prefix.length).trim();
|
|
71
|
+
}
|
|
72
|
+
return text ? { basis: text, negative } : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readOwnDataProperty(value, fieldName, subject) {
|
|
76
|
+
let descriptor;
|
|
77
|
+
try {
|
|
78
|
+
descriptor = Object.getOwnPropertyDescriptor(value, fieldName);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new TypeError(`${subject} ${fieldName} must be an own data property`);
|
|
81
|
+
}
|
|
82
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
83
|
+
throw new TypeError(`${subject} ${fieldName} must be an own data property`);
|
|
84
|
+
}
|
|
85
|
+
return descriptor.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stalenessForMemory(memory, usageProjection, options = {}) {
|
|
89
|
+
if (!memory || typeof memory !== 'object') {
|
|
90
|
+
throw new TypeError('memory must be an object');
|
|
91
|
+
}
|
|
92
|
+
const assertCanonicalIso = (value, fieldName) => {
|
|
93
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))
|
|
94
|
+
|| new Date(value).toISOString() !== value) {
|
|
95
|
+
throw new TypeError(`${fieldName} must be a canonical ISO timestamp`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const createdAt = readOwnDataProperty(memory, 'created_at', 'memory');
|
|
99
|
+
assertCanonicalIso(createdAt, 'memory created_at');
|
|
100
|
+
let useCount = 0;
|
|
101
|
+
let baselineAt = createdAt;
|
|
102
|
+
if (usageProjection !== null && usageProjection !== undefined) {
|
|
103
|
+
if (!usageProjection || typeof usageProjection !== 'object') {
|
|
104
|
+
throw new TypeError('durable usage projection must be an object');
|
|
105
|
+
}
|
|
106
|
+
const lastUsedAt = readOwnDataProperty(usageProjection, 'last_used_at', 'durable usage projection');
|
|
107
|
+
const projectedUseCount = readOwnDataProperty(usageProjection, 'use_count', 'durable usage projection');
|
|
108
|
+
assertCanonicalIso(lastUsedAt, 'durable last_used_at');
|
|
109
|
+
if (!Number.isSafeInteger(projectedUseCount) || projectedUseCount < 0) {
|
|
110
|
+
throw new TypeError('durable usage projection use_count must be a nonnegative safe integer');
|
|
111
|
+
}
|
|
112
|
+
baselineAt = lastUsedAt;
|
|
113
|
+
useCount = projectedUseCount;
|
|
114
|
+
}
|
|
115
|
+
if (!options || typeof options !== 'object') {
|
|
116
|
+
throw new TypeError('staleness review options must be an object');
|
|
117
|
+
}
|
|
118
|
+
const now = readOwnDataProperty(options, 'now', 'staleness review options');
|
|
119
|
+
assertCanonicalIso(now, 'staleness review now');
|
|
120
|
+
const ageMs = Date.parse(now) - Date.parse(baselineAt);
|
|
121
|
+
const stale = ageMs > STALE_AFTER_MS;
|
|
122
|
+
return {
|
|
123
|
+
baseline_at: baselineAt,
|
|
124
|
+
use_count: useCount,
|
|
125
|
+
stale,
|
|
126
|
+
demote: stale,
|
|
127
|
+
age_days: Math.max(0, Math.floor(ageMs / (24 * 60 * 60 * 1000))),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function reviewMemories(entries, options = {}) {
|
|
132
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
133
|
+
const entryLimit = Math.max(1, Math.min(options.entryLimit || MEMORY_REVIEW_ENTRY_LIMIT, MEMORY_REVIEW_ENTRY_LIMIT));
|
|
134
|
+
const findingLimit = Math.max(1, Math.min(options.findingLimit || DEFAULT_FINDING_LIMIT, DEFAULT_FINDING_LIMIT));
|
|
135
|
+
const selected = source.slice(0, entryLimit);
|
|
136
|
+
const findings = [];
|
|
137
|
+
|
|
138
|
+
const duplicateGroups = new Map();
|
|
139
|
+
for (const entry of selected) {
|
|
140
|
+
const payload = payloadFor(entry);
|
|
141
|
+
const canonical = JSON.stringify(canonicalize(payload));
|
|
142
|
+
const group = duplicateGroups.get(canonical) || [];
|
|
143
|
+
group.push(fingerprint(payload));
|
|
144
|
+
duplicateGroups.set(canonical, group);
|
|
145
|
+
}
|
|
146
|
+
for (const [canonical, members] of duplicateGroups) {
|
|
147
|
+
if (members.length < 2) continue;
|
|
148
|
+
findings.push({
|
|
149
|
+
review_id: reviewId('duplicate', [canonical]),
|
|
150
|
+
kind: 'duplicate',
|
|
151
|
+
count: members.length,
|
|
152
|
+
members: [...new Set(members)].sort(compareCodeUnits),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const claims = new Map();
|
|
157
|
+
for (const entry of selected) {
|
|
158
|
+
const claim = claimFor(entry);
|
|
159
|
+
if (!claim) continue;
|
|
160
|
+
const sides = claims.get(claim.basis) || { positive: [], negative: [] };
|
|
161
|
+
sides[claim.negative ? 'negative' : 'positive'].push(fingerprint(payloadFor(entry)));
|
|
162
|
+
claims.set(claim.basis, sides);
|
|
163
|
+
}
|
|
164
|
+
for (const [basis, sides] of claims) {
|
|
165
|
+
if (!sides.positive.length || !sides.negative.length) continue;
|
|
166
|
+
findings.push({
|
|
167
|
+
review_id: reviewId('contradiction', [basis]),
|
|
168
|
+
kind: 'contradiction',
|
|
169
|
+
claim: basis,
|
|
170
|
+
members: [...new Set([...sides.positive, ...sides.negative])].sort(compareCodeUnits),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
findings.sort((left, right) => compareCodeUnits(left.review_id, right.review_id));
|
|
175
|
+
const total = Number.isInteger(options.total) && options.total >= source.length
|
|
176
|
+
? options.total
|
|
177
|
+
: source.length;
|
|
178
|
+
return {
|
|
179
|
+
total,
|
|
180
|
+
scanned: selected.length,
|
|
181
|
+
truncated: total > selected.length || findings.length > findingLimit,
|
|
182
|
+
findings: findings.slice(0, findingLimit),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
MEMORY_REVIEW_ENTRY_LIMIT,
|
|
188
|
+
STALE_AFTER_DAYS,
|
|
189
|
+
reviewMemories,
|
|
190
|
+
stalenessForMemory,
|
|
191
|
+
};
|