cclaw-cli 0.5.17 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +6 -2
- package/dist/cli.js +45 -5
- package/dist/config.d.ts +12 -2
- package/dist/config.js +79 -5
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +3 -2
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +145 -0
- package/dist/content/learnings.js +91 -18
- package/dist/content/meta-skill.js +52 -3
- package/dist/content/next-command.js +8 -0
- package/dist/content/observe.js +18 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/stage-schema.d.ts +18 -1
- package/dist/content/stage-schema.js +36 -10
- package/dist/content/start-command.js +30 -7
- package/dist/content/status-command.d.ts +9 -0
- package/dist/content/status-command.js +154 -0
- package/dist/content/templates.js +41 -5
- package/dist/content/utility-skills.d.ts +16 -2
- package/dist/content/utility-skills.js +721 -3
- package/dist/delegation.d.ts +6 -1
- package/dist/delegation.js +3 -2
- package/dist/doctor.js +38 -1
- package/dist/flow-state.d.ts +16 -4
- package/dist/flow-state.js +50 -11
- package/dist/harness-adapters.js +1 -0
- package/dist/install.d.ts +4 -1
- package/dist/install.js +174 -10
- package/dist/policy.js +2 -1
- package/dist/runs.d.ts +15 -0
- package/dist/runs.js +59 -4
- package/dist/types.d.ts +42 -0
- package/dist/types.js +33 -0
- package/package.json +1 -1
package/dist/delegation.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { FlowStage } from "./types.js";
|
|
|
2
2
|
export type DelegationEntry = {
|
|
3
3
|
stage: string;
|
|
4
4
|
agent: string;
|
|
5
|
-
mode: "mandatory" | "proactive";
|
|
5
|
+
mode: "mandatory" | "proactive" | "conditional";
|
|
6
6
|
status: "scheduled" | "completed" | "failed" | "waived";
|
|
7
7
|
taskId?: string;
|
|
8
8
|
waiverReason?: string;
|
|
@@ -12,6 +12,11 @@ export type DelegationEntry = {
|
|
|
12
12
|
* consumers treat missing runId as unscoped (conservatively excluded from current-run checks).
|
|
13
13
|
*/
|
|
14
14
|
runId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* For `conditional` rows: the trigger predicate that fired (e.g. `diff_lines_gt:100`).
|
|
17
|
+
* Recorded for audit so reviewers can see why the second pass was required.
|
|
18
|
+
*/
|
|
19
|
+
conditionTrigger?: string;
|
|
15
20
|
};
|
|
16
21
|
export type DelegationLedger = {
|
|
17
22
|
runId: string;
|
package/dist/delegation.js
CHANGED
|
@@ -14,7 +14,7 @@ function isDelegationEntry(value) {
|
|
|
14
14
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
15
|
return false;
|
|
16
16
|
const o = value;
|
|
17
|
-
const modeOk = o.mode === "mandatory" || o.mode === "proactive";
|
|
17
|
+
const modeOk = o.mode === "mandatory" || o.mode === "proactive" || o.mode === "conditional";
|
|
18
18
|
const statusOk = o.status === "scheduled" ||
|
|
19
19
|
o.status === "completed" ||
|
|
20
20
|
o.status === "failed" ||
|
|
@@ -26,7 +26,8 @@ function isDelegationEntry(value) {
|
|
|
26
26
|
typeof o.ts === "string" &&
|
|
27
27
|
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
28
28
|
(o.waiverReason === undefined || typeof o.waiverReason === "string") &&
|
|
29
|
-
(o.runId === undefined || typeof o.runId === "string")
|
|
29
|
+
(o.runId === undefined || typeof o.runId === "string") &&
|
|
30
|
+
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string"));
|
|
30
31
|
}
|
|
31
32
|
function parseLedger(raw, runId) {
|
|
32
33
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
package/dist/doctor.js
CHANGED
|
@@ -11,11 +11,13 @@ import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
|
11
11
|
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END } from "./harness-adapters.js";
|
|
12
12
|
import { policyChecks } from "./policy.js";
|
|
13
13
|
import { readFlowState } from "./runs.js";
|
|
14
|
+
import { skippedStagesForTrack } from "./flow-state.js";
|
|
15
|
+
import { TRACK_STAGES } from "./types.js";
|
|
14
16
|
import { checkMandatoryDelegations } from "./delegation.js";
|
|
15
17
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
16
18
|
import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
17
19
|
import { stageSkillFolder } from "./content/skills.js";
|
|
18
|
-
import { UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
20
|
+
import { LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
19
21
|
import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
|
|
20
22
|
import { validateHookDocument } from "./hook-schema.js";
|
|
21
23
|
const execFileAsync = promisify(execFile);
|
|
@@ -406,6 +408,18 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
406
408
|
details: skillPath
|
|
407
409
|
});
|
|
408
410
|
}
|
|
411
|
+
// Opt-in language rule packs: only check presence for packs the user enabled.
|
|
412
|
+
for (const pack of parsedConfig?.languageRulePacks ?? []) {
|
|
413
|
+
const folder = LANGUAGE_RULE_PACK_FOLDERS[pack];
|
|
414
|
+
if (!folder)
|
|
415
|
+
continue;
|
|
416
|
+
const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", folder, "SKILL.md");
|
|
417
|
+
checks.push({
|
|
418
|
+
name: `language_rule_pack:${pack}`,
|
|
419
|
+
ok: await exists(skillPath),
|
|
420
|
+
details: skillPath
|
|
421
|
+
});
|
|
422
|
+
}
|
|
409
423
|
// Agent definition files
|
|
410
424
|
for (const agent of CCLAW_AGENTS) {
|
|
411
425
|
const agentPath = path.join(projectRoot, RUNTIME_ROOT, "agents", `${agent.name}.md`);
|
|
@@ -743,6 +757,29 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
743
757
|
ok: activeRunId.length > 0,
|
|
744
758
|
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
745
759
|
});
|
|
760
|
+
const activeTrack = flowState.track ?? "standard";
|
|
761
|
+
const trackStageList = TRACK_STAGES[activeTrack];
|
|
762
|
+
const skippedFromState = Array.isArray(flowState.skippedStages) ? flowState.skippedStages : [];
|
|
763
|
+
const expectedSkipped = skippedStagesForTrack(activeTrack);
|
|
764
|
+
const skippedConsistent = expectedSkipped.length === skippedFromState.length &&
|
|
765
|
+
expectedSkipped.every((stage) => skippedFromState.includes(stage));
|
|
766
|
+
checks.push({
|
|
767
|
+
name: "flow_state:track",
|
|
768
|
+
ok: skippedConsistent,
|
|
769
|
+
details: skippedConsistent
|
|
770
|
+
? `active track "${activeTrack}" (${trackStageList.length}/${COMMAND_FILE_ORDER.length} stages: ${trackStageList.join(" → ")})${expectedSkipped.length > 0 ? `; skippedStages=${expectedSkipped.join(", ")}` : ""}`
|
|
771
|
+
: `track "${activeTrack}" expects skippedStages=[${expectedSkipped.join(", ")}] but flow-state has [${skippedFromState.join(", ")}] — run \`cclaw sync\` to repair`
|
|
772
|
+
});
|
|
773
|
+
checks.push({
|
|
774
|
+
name: "flow_state:track_completed_in_track",
|
|
775
|
+
ok: flowState.completedStages.every((stage) => trackStageList.includes(stage) || expectedSkipped.includes(stage)),
|
|
776
|
+
details: (() => {
|
|
777
|
+
const offTrack = flowState.completedStages.filter((stage) => !trackStageList.includes(stage) && !expectedSkipped.includes(stage));
|
|
778
|
+
return offTrack.length === 0
|
|
779
|
+
? `every completed stage belongs to track "${activeTrack}" or its skipped set`
|
|
780
|
+
: `completed stages contain entries outside track "${activeTrack}" and not in skipped set: ${offTrack.join(", ")}`;
|
|
781
|
+
})()
|
|
782
|
+
});
|
|
746
783
|
checks.push({
|
|
747
784
|
name: "artifacts:active_root",
|
|
748
785
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowStage, TransitionRule } from "./types.js";
|
|
1
|
+
import type { FlowStage, FlowTrack, TransitionRule } from "./types.js";
|
|
2
2
|
export declare const TRANSITION_RULES: TransitionRule[];
|
|
3
3
|
export interface StageGateState {
|
|
4
4
|
required: string[];
|
|
@@ -11,9 +11,21 @@ export interface FlowState {
|
|
|
11
11
|
completedStages: FlowStage[];
|
|
12
12
|
guardEvidence: Record<string, string>;
|
|
13
13
|
stageGateCatalog: Record<FlowStage, StageGateState>;
|
|
14
|
+
/** Active flow track (determines which stages are in the critical path for this run). */
|
|
15
|
+
track: FlowTrack;
|
|
16
|
+
/** Stages explicitly skipped for this track (empty for standard; populated for quick). */
|
|
17
|
+
skippedStages: FlowStage[];
|
|
14
18
|
}
|
|
15
|
-
export
|
|
19
|
+
export interface InitialFlowStateOptions {
|
|
20
|
+
activeRunId?: string;
|
|
21
|
+
track?: FlowTrack;
|
|
22
|
+
}
|
|
23
|
+
export declare function isFlowTrack(value: unknown): value is FlowTrack;
|
|
24
|
+
export declare function trackStages(track: FlowTrack): FlowStage[];
|
|
25
|
+
export declare function skippedStagesForTrack(track: FlowTrack): FlowStage[];
|
|
26
|
+
export declare function firstStageForTrack(track: FlowTrack): FlowStage;
|
|
27
|
+
export declare function createInitialFlowState(activeRunIdOrOptions?: string | InitialFlowStateOptions, maybeTrack?: FlowTrack): FlowState;
|
|
16
28
|
export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
|
|
17
29
|
export declare function getTransitionGuards(from: FlowStage, to: FlowStage): string[];
|
|
18
|
-
export declare function nextStage(stage: FlowStage): FlowStage | null;
|
|
19
|
-
export declare function previousStage(stage: FlowStage): FlowStage | null;
|
|
30
|
+
export declare function nextStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
|
31
|
+
export declare function previousStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
package/dist/flow-state.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { COMMAND_FILE_ORDER } from "./constants.js";
|
|
2
2
|
import { buildTransitionRules, orderedStageSchemas, stageGateIds } from "./content/stage-schema.js";
|
|
3
|
+
import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
|
|
3
4
|
export const TRANSITION_RULES = buildTransitionRules();
|
|
4
|
-
export function
|
|
5
|
+
export function isFlowTrack(value) {
|
|
6
|
+
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
7
|
+
}
|
|
8
|
+
export function trackStages(track) {
|
|
9
|
+
return [...TRACK_STAGES[track]];
|
|
10
|
+
}
|
|
11
|
+
export function skippedStagesForTrack(track) {
|
|
12
|
+
const inTrack = new Set(TRACK_STAGES[track]);
|
|
13
|
+
return FLOW_STAGES.filter((stage) => !inTrack.has(stage));
|
|
14
|
+
}
|
|
15
|
+
export function firstStageForTrack(track) {
|
|
16
|
+
const stages = TRACK_STAGES[track];
|
|
17
|
+
return stages[0] ?? "brainstorm";
|
|
18
|
+
}
|
|
19
|
+
export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTrack) {
|
|
20
|
+
const options = typeof activeRunIdOrOptions === "string"
|
|
21
|
+
? { activeRunId: activeRunIdOrOptions, track: maybeTrack }
|
|
22
|
+
: activeRunIdOrOptions;
|
|
23
|
+
const activeRunId = options.activeRunId ?? "active";
|
|
24
|
+
const track = options.track ?? "standard";
|
|
25
|
+
const skippedStages = skippedStagesForTrack(track);
|
|
5
26
|
const stageGateCatalog = {};
|
|
6
27
|
for (const schema of orderedStageSchemas()) {
|
|
7
28
|
stageGateCatalog[schema.stage] = {
|
|
@@ -12,10 +33,12 @@ export function createInitialFlowState(activeRunId = "active") {
|
|
|
12
33
|
}
|
|
13
34
|
return {
|
|
14
35
|
activeRunId,
|
|
15
|
-
currentStage:
|
|
36
|
+
currentStage: firstStageForTrack(track),
|
|
16
37
|
completedStages: [],
|
|
17
38
|
guardEvidence: {},
|
|
18
|
-
stageGateCatalog
|
|
39
|
+
stageGateCatalog,
|
|
40
|
+
track,
|
|
41
|
+
skippedStages
|
|
19
42
|
};
|
|
20
43
|
}
|
|
21
44
|
export function canTransition(from, to) {
|
|
@@ -25,17 +48,33 @@ export function getTransitionGuards(from, to) {
|
|
|
25
48
|
const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
|
|
26
49
|
return match ? [...match.guards] : [];
|
|
27
50
|
}
|
|
28
|
-
export function nextStage(stage) {
|
|
29
|
-
const
|
|
30
|
-
|
|
51
|
+
export function nextStage(stage, track = "standard") {
|
|
52
|
+
const ordered = TRACK_STAGES[track];
|
|
53
|
+
const index = ordered.indexOf(stage);
|
|
54
|
+
if (index < 0) {
|
|
55
|
+
const fallback = COMMAND_FILE_ORDER.indexOf(stage);
|
|
56
|
+
if (fallback < 0 || fallback === COMMAND_FILE_ORDER.length - 1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return COMMAND_FILE_ORDER[fallback + 1];
|
|
60
|
+
}
|
|
61
|
+
if (index === ordered.length - 1) {
|
|
31
62
|
return null;
|
|
32
63
|
}
|
|
33
|
-
return
|
|
64
|
+
return ordered[index + 1];
|
|
34
65
|
}
|
|
35
|
-
export function previousStage(stage) {
|
|
36
|
-
const
|
|
37
|
-
|
|
66
|
+
export function previousStage(stage, track = "standard") {
|
|
67
|
+
const ordered = TRACK_STAGES[track];
|
|
68
|
+
const index = ordered.indexOf(stage);
|
|
69
|
+
if (index === 0) {
|
|
38
70
|
return null;
|
|
39
71
|
}
|
|
40
|
-
|
|
72
|
+
if (index < 0) {
|
|
73
|
+
const fallback = COMMAND_FILE_ORDER.indexOf(stage);
|
|
74
|
+
if (fallback <= 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return COMMAND_FILE_ORDER[fallback - 1];
|
|
78
|
+
}
|
|
79
|
+
return ordered[index - 1];
|
|
41
80
|
}
|
package/dist/harness-adapters.js
CHANGED
|
@@ -124,6 +124,7 @@ export async function syncHarnessShims(projectRoot, harnesses) {
|
|
|
124
124
|
await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
|
|
125
125
|
await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
|
|
126
126
|
await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
|
|
127
|
+
await writeFileSafe(path.join(commandDir, "cc-status.md"), utilityShimContent(harness, "status", "flow-status", "status.md"));
|
|
127
128
|
}
|
|
128
129
|
await syncAgentFiles(projectRoot);
|
|
129
130
|
await syncAgentsMd(projectRoot);
|
package/dist/install.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { HarnessId } from "./types.js";
|
|
1
|
+
import type { FlowTrack, HarnessId, InitProfile } from "./types.js";
|
|
2
2
|
export interface InitOptions {
|
|
3
3
|
projectRoot: string;
|
|
4
4
|
harnesses?: HarnessId[];
|
|
5
|
+
track?: FlowTrack;
|
|
6
|
+
/** When set, pre-fills config defaults from the named profile before applying flag overrides. */
|
|
7
|
+
profile?: InitProfile;
|
|
5
8
|
}
|
|
6
9
|
export declare function initCclaw(options: InitOptions): Promise<void>;
|
|
7
10
|
export declare function syncCclaw(projectRoot: string): Promise<void>;
|
package/dist/install.js
CHANGED
|
@@ -3,20 +3,21 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
6
|
-
import { writeConfig, createDefaultConfig, readConfig, configPath } from "./config.js";
|
|
6
|
+
import { writeConfig, createDefaultConfig, createProfileConfig, readConfig, configPath } from "./config.js";
|
|
7
7
|
import { commandContract } from "./content/contracts.js";
|
|
8
8
|
import { contextModeFiles, createInitialContextModeState } from "./content/contexts.js";
|
|
9
9
|
import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
|
|
10
10
|
import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
|
|
11
11
|
import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
|
|
12
|
+
import { statusCommandContract, statusCommandSkillMarkdown } from "./content/status-command.js";
|
|
12
13
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
13
14
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
14
|
-
import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
15
|
+
import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
15
16
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
16
17
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
17
18
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
18
19
|
import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
19
|
-
import { UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
|
|
20
|
+
import { LANGUAGE_RULE_PACK_FOLDERS, LANGUAGE_RULE_PACK_GENERATORS, UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
|
|
20
21
|
import { createInitialFlowState } from "./flow-state.js";
|
|
21
22
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
22
23
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
@@ -164,7 +165,7 @@ async function writeArtifactTemplates(projectRoot) {
|
|
|
164
165
|
await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
async function writeSkills(projectRoot) {
|
|
168
|
+
async function writeSkills(projectRoot, config) {
|
|
168
169
|
for (const stage of COMMAND_FILE_ORDER) {
|
|
169
170
|
const folder = stageSkillFolder(stage);
|
|
170
171
|
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), stageSkillMarkdown(stage));
|
|
@@ -173,6 +174,7 @@ async function writeSkills(projectRoot) {
|
|
|
173
174
|
await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
|
|
174
175
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
|
|
175
176
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
|
|
177
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-status", "SKILL.md"), statusCommandSkillMarkdown());
|
|
176
178
|
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
177
179
|
await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
|
|
178
180
|
await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
|
|
@@ -181,11 +183,20 @@ async function writeSkills(projectRoot) {
|
|
|
181
183
|
const generator = UTILITY_SKILL_MAP[folder];
|
|
182
184
|
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
|
|
183
185
|
}
|
|
186
|
+
const enabledPacks = config?.languageRulePacks ?? [];
|
|
187
|
+
for (const pack of enabledPacks) {
|
|
188
|
+
const folder = LANGUAGE_RULE_PACK_FOLDERS[pack];
|
|
189
|
+
const generator = LANGUAGE_RULE_PACK_GENERATORS[folder];
|
|
190
|
+
if (!folder || !generator)
|
|
191
|
+
continue;
|
|
192
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), generator());
|
|
193
|
+
}
|
|
184
194
|
}
|
|
185
195
|
async function writeUtilityCommands(projectRoot) {
|
|
186
196
|
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
187
197
|
await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
|
|
188
198
|
await writeFileSafe(runtimePath(projectRoot, "commands", "start.md"), startCommandContract());
|
|
199
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "status.md"), statusCommandContract());
|
|
189
200
|
}
|
|
190
201
|
function toObject(value) {
|
|
191
202
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -482,6 +493,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
482
493
|
await ensureDir(hooksDir);
|
|
483
494
|
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
484
495
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
496
|
+
await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
|
|
485
497
|
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
|
|
486
498
|
strictMode: config.promptGuardMode === "strict"
|
|
487
499
|
}));
|
|
@@ -493,6 +505,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
493
505
|
for (const script of [
|
|
494
506
|
"session-start.sh",
|
|
495
507
|
"stop-checkpoint.sh",
|
|
508
|
+
"pre-compact.sh",
|
|
496
509
|
"prompt-guard.sh",
|
|
497
510
|
"workflow-guard.sh",
|
|
498
511
|
"context-monitor.sh",
|
|
@@ -542,6 +555,151 @@ async function ensureKnowledgeStore(projectRoot) {
|
|
|
542
555
|
await writeFileSafe(storePath, "# Project Knowledge\n\n");
|
|
543
556
|
}
|
|
544
557
|
}
|
|
558
|
+
async function ensureCustomSkillsScaffold(projectRoot) {
|
|
559
|
+
const customDir = runtimePath(projectRoot, "custom-skills");
|
|
560
|
+
await ensureDir(customDir);
|
|
561
|
+
const readmePath = path.join(customDir, "README.md");
|
|
562
|
+
if (!(await exists(readmePath))) {
|
|
563
|
+
await writeFileSafe(readmePath, CUSTOM_SKILLS_README);
|
|
564
|
+
}
|
|
565
|
+
const examplePath = path.join(customDir, "example", "SKILL.md");
|
|
566
|
+
if (!(await exists(examplePath))) {
|
|
567
|
+
await writeFileSafe(examplePath, CUSTOM_SKILLS_EXAMPLE);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const CUSTOM_SKILLS_README = `# Custom Skills (sync-safe)
|
|
571
|
+
|
|
572
|
+
This directory is **never overwritten** by \`cclaw sync\` or \`cclaw upgrade\`. Use it
|
|
573
|
+
to add project-specific skills that complement the managed skills under
|
|
574
|
+
\`.cclaw/skills/\`.
|
|
575
|
+
|
|
576
|
+
## When to add a custom skill
|
|
577
|
+
|
|
578
|
+
- A repeatable lens specific to **this project** (e.g. "billing-domain", "kafka-message-contracts").
|
|
579
|
+
- A team convention you want every agent session to load.
|
|
580
|
+
- A domain checklist that does not generalize to other projects.
|
|
581
|
+
|
|
582
|
+
If the skill is general (security, performance, accessibility, etc.) prefer
|
|
583
|
+
contributing it upstream instead — the managed skills receive maintenance.
|
|
584
|
+
|
|
585
|
+
## File format — public API (stable contract)
|
|
586
|
+
|
|
587
|
+
Each skill lives at \`.cclaw/custom-skills/<folder>/SKILL.md\`. The format is a
|
|
588
|
+
**stable public API**: \`cclaw sync\` and \`cclaw upgrade\` will not rewrite
|
|
589
|
+
custom skills, and the fields below are guaranteed to be respected by the
|
|
590
|
+
meta-skill router and the stage hooks.
|
|
591
|
+
|
|
592
|
+
### Frontmatter (YAML, required)
|
|
593
|
+
|
|
594
|
+
\`\`\`yaml
|
|
595
|
+
---
|
|
596
|
+
# Required fields
|
|
597
|
+
name: <kebab-case-skill-name>
|
|
598
|
+
description: >
|
|
599
|
+
One sentence (≤180 chars) that triggers semantic routing. Include the
|
|
600
|
+
concrete situation and the expected action
|
|
601
|
+
(e.g. "Audit Kafka topic contracts when a producer or consumer signature changes").
|
|
602
|
+
|
|
603
|
+
# Optional fields (omit when not applicable)
|
|
604
|
+
stages: [design, spec, tdd, review] # flow stages this skill applies to
|
|
605
|
+
triggers:
|
|
606
|
+
- "kafka topic"
|
|
607
|
+
- "producer.schema"
|
|
608
|
+
- "consumer.schema"
|
|
609
|
+
hardGate: false # true => skill body MUST include a ## HARD-GATE section
|
|
610
|
+
owners: ["@team-messaging"] # informational routing hint, not enforced
|
|
611
|
+
version: 0.1.0 # semver; bump when hardGate or algorithm changes
|
|
612
|
+
---
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
### Field contract
|
|
616
|
+
|
|
617
|
+
| Field | Type | Required | Meaning |
|
|
618
|
+
|---|---|---|---|
|
|
619
|
+
| \`name\` | string (kebab-case) | yes | Unique id used by the router and by \`/cc-status\` diagnostics. |
|
|
620
|
+
| \`description\` | string ≤180 chars (single line OR YAML \`>\` folded) | yes | Drives semantic routing. Include trigger + action. |
|
|
621
|
+
| \`stages\` | array of flow stages | no | When present, the meta-skill only surfaces this skill during those stages. Omit for "any stage". |
|
|
622
|
+
| \`triggers\` | array of strings | no | Extra literal substrings that route to this skill when found in the user prompt or the active artifact. |
|
|
623
|
+
| \`hardGate\` | boolean | no | When \`true\`, the body MUST include a \`## HARD-GATE\` section; the agent treats the rule as non-skippable. |
|
|
624
|
+
| \`owners\` | array of strings | no | Informational only — surfaced to the user, never enforced. |
|
|
625
|
+
| \`version\` | semver string | no | Bump when you change the HARD-GATE or algorithm so reviewers can spot changes. |
|
|
626
|
+
|
|
627
|
+
### Body sections (markdown, recommended order)
|
|
628
|
+
|
|
629
|
+
\`\`\`markdown
|
|
630
|
+
# <Skill title>
|
|
631
|
+
|
|
632
|
+
## Overview
|
|
633
|
+
One-paragraph summary; context for when this skill is loaded.
|
|
634
|
+
|
|
635
|
+
## When to use
|
|
636
|
+
- Bullet list of situations where this skill adds value.
|
|
637
|
+
|
|
638
|
+
## When NOT to use
|
|
639
|
+
- Situations where loading this skill is context bloat or wrong.
|
|
640
|
+
|
|
641
|
+
## HARD-GATE (REQUIRED when frontmatter hardGate: true)
|
|
642
|
+
Phrase it as a refusal:
|
|
643
|
+
> Do not <X> while <Y>.
|
|
644
|
+
|
|
645
|
+
## Algorithm / checklist
|
|
646
|
+
1. Concrete, observable steps with evidence (file:line, artifact, or knowledge entry).
|
|
647
|
+
|
|
648
|
+
## Output protocol
|
|
649
|
+
Where the artifact / chat output lives and what shape it takes.
|
|
650
|
+
|
|
651
|
+
## Anti-patterns
|
|
652
|
+
- Common failure modes to reject.
|
|
653
|
+
\`\`\`
|
|
654
|
+
|
|
655
|
+
### Stage association semantics
|
|
656
|
+
|
|
657
|
+
- \`stages: []\` or missing → skill is available at any stage. The meta-skill still only surfaces it when \`description\` or \`triggers\` match the prompt.
|
|
658
|
+
- \`stages: [review]\` → skill is offered only during the review stage.
|
|
659
|
+
- Custom skills **never** become mandatory delegations. They are opt-in lenses. If you need a mandatory dispatch, add a proper managed specialist under \`.cclaw/skills/\` instead.
|
|
660
|
+
|
|
661
|
+
## Routing
|
|
662
|
+
|
|
663
|
+
Custom skills are surfaced via the \`using-cclaw\` meta-skill at session start.
|
|
664
|
+
Mention the skill name in your prompt or let the agent semantic-route to it
|
|
665
|
+
based on the description + triggers + stages frontmatter.
|
|
666
|
+
|
|
667
|
+
## Versioning & removal
|
|
668
|
+
|
|
669
|
+
Custom skills are user-owned. Bump \`version\` when you change the HARD-GATE or
|
|
670
|
+
algorithm; delete or edit them at any time — \`cclaw sync\` will not touch them.
|
|
671
|
+
`;
|
|
672
|
+
const CUSTOM_SKILLS_EXAMPLE = `---
|
|
673
|
+
name: example-custom-skill
|
|
674
|
+
description: "Replace this with a one-sentence description that triggers when the skill should be used. Delete or rename this folder when you add a real skill."
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
# Example Custom Skill
|
|
678
|
+
|
|
679
|
+
This is a placeholder. Use it as a starting template, then delete or rename
|
|
680
|
+
the \`example/\` folder.
|
|
681
|
+
|
|
682
|
+
## When to use
|
|
683
|
+
|
|
684
|
+
- A real, repeatable situation in **this** project that needs a consistent lens.
|
|
685
|
+
|
|
686
|
+
## HARD-GATE (optional)
|
|
687
|
+
|
|
688
|
+
Drop this section if no hard rule applies. Keep it crisp:
|
|
689
|
+
|
|
690
|
+
> Do not <X> while <Y>.
|
|
691
|
+
|
|
692
|
+
## Algorithm
|
|
693
|
+
|
|
694
|
+
1. Step one — observable, file:line evidence required.
|
|
695
|
+
2. Step two — produce a named artifact, not a vibe.
|
|
696
|
+
3. Step three — escalate / hand off / record knowledge entry.
|
|
697
|
+
|
|
698
|
+
## Anti-patterns
|
|
699
|
+
|
|
700
|
+
- Treating this skill as advisory when the situation matches the trigger.
|
|
701
|
+
- Loading this skill when the situation clearly does not match (context bloat).
|
|
702
|
+
`;
|
|
545
703
|
async function ensureSessionStateFiles(projectRoot) {
|
|
546
704
|
const stateDir = runtimePath(projectRoot, "state");
|
|
547
705
|
await ensureDir(stateDir);
|
|
@@ -623,12 +781,12 @@ async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
|
|
|
623
781
|
await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
624
782
|
}
|
|
625
783
|
}
|
|
626
|
-
async function writeState(projectRoot, forceReset = false) {
|
|
784
|
+
async function writeState(projectRoot, config, forceReset = false) {
|
|
627
785
|
const statePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
628
786
|
if (!forceReset && (await exists(statePath))) {
|
|
629
787
|
return;
|
|
630
788
|
}
|
|
631
|
-
const state = createInitialFlowState();
|
|
789
|
+
const state = createInitialFlowState("active", config.defaultTrack ?? "standard");
|
|
632
790
|
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
633
791
|
}
|
|
634
792
|
async function writeAdapterManifest(projectRoot, harnesses) {
|
|
@@ -736,15 +894,16 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
736
894
|
await cleanStaleFiles(projectRoot);
|
|
737
895
|
await writeCommandContracts(projectRoot);
|
|
738
896
|
await writeUtilityCommands(projectRoot);
|
|
739
|
-
await writeSkills(projectRoot);
|
|
897
|
+
await writeSkills(projectRoot, config);
|
|
740
898
|
await writeContextModes(projectRoot);
|
|
741
899
|
await writeArtifactTemplates(projectRoot);
|
|
742
900
|
await writeRulebook(projectRoot);
|
|
743
|
-
await writeState(projectRoot, forceStateReset);
|
|
901
|
+
await writeState(projectRoot, config, forceStateReset);
|
|
744
902
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
745
903
|
await ensureSessionStateFiles(projectRoot);
|
|
746
904
|
await writeAdapterManifest(projectRoot, harnesses);
|
|
747
905
|
await ensureKnowledgeStore(projectRoot);
|
|
906
|
+
await ensureCustomSkillsScaffold(projectRoot);
|
|
748
907
|
await writeHooks(projectRoot, config);
|
|
749
908
|
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
750
909
|
await syncManagedGitHooks(projectRoot, config);
|
|
@@ -753,7 +912,12 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
753
912
|
await ensureGitignore(projectRoot);
|
|
754
913
|
}
|
|
755
914
|
export async function initCclaw(options) {
|
|
756
|
-
const config =
|
|
915
|
+
const config = options.profile
|
|
916
|
+
? createProfileConfig(options.profile, {
|
|
917
|
+
harnesses: options.harnesses,
|
|
918
|
+
defaultTrack: options.track
|
|
919
|
+
})
|
|
920
|
+
: createDefaultConfig(options.harnesses, options.track);
|
|
757
921
|
await writeConfig(options.projectRoot, config);
|
|
758
922
|
await materializeRuntime(options.projectRoot, config, true);
|
|
759
923
|
}
|
|
@@ -828,7 +992,7 @@ function stripManagedHookCommands(value) {
|
|
|
828
992
|
}
|
|
829
993
|
function isManagedRuntimeHookCommand(command) {
|
|
830
994
|
const normalized = command.trim().replace(/\s+/gu, " ");
|
|
831
|
-
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
|
|
995
|
+
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
|
|
832
996
|
}
|
|
833
997
|
async function removeManagedHookEntries(hookFilePath) {
|
|
834
998
|
if (!(await exists(hookFilePath)))
|
package/dist/policy.js
CHANGED
|
@@ -85,7 +85,8 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
85
85
|
// --- utility skill checks ---
|
|
86
86
|
const runtimeFile = (relativePath) => `${RUNTIME_ROOT}/${relativePath}`;
|
|
87
87
|
const utilitySkillChecks = [
|
|
88
|
-
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Entry format
|
|
88
|
+
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Entry format", name: "utility_skill:learnings:entry_format" },
|
|
89
|
+
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "knowledge.jsonl", name: "utility_skill:learnings:jsonl_mirror" },
|
|
89
90
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Subcommands", name: "utility_skill:learnings:subcommands" },
|
|
90
91
|
{ file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
|
|
91
92
|
{ file: runtimeFile("commands/learn.md"), needle: "## Subcommands", name: "utility_command:learn:subcommands" },
|
package/dist/runs.d.ts
CHANGED
|
@@ -24,6 +24,13 @@ export interface ArchiveRunResult {
|
|
|
24
24
|
featureName: string;
|
|
25
25
|
resetState: FlowState;
|
|
26
26
|
snapshottedStateFiles: string[];
|
|
27
|
+
/** Knowledge curation hint: total active entries + soft threshold (50). */
|
|
28
|
+
knowledge: {
|
|
29
|
+
activeEntryCount: number;
|
|
30
|
+
softThreshold: number;
|
|
31
|
+
overThreshold: boolean;
|
|
32
|
+
knowledgePath: string;
|
|
33
|
+
};
|
|
27
34
|
}
|
|
28
35
|
export interface ArchiveManifest {
|
|
29
36
|
version: 1;
|
|
@@ -48,4 +55,12 @@ export declare function writeFlowState(projectRoot: string, state: FlowState, op
|
|
|
48
55
|
export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
|
|
49
56
|
export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
|
|
50
57
|
export declare function archiveRun(projectRoot: string, featureName?: string): Promise<ArchiveRunResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Counts active (non-superseded) knowledge entries.
|
|
60
|
+
* An entry is a markdown H3 heading with the canonical timestamped format produced by
|
|
61
|
+
* `learn add` / `learn curate`. Entries marked `Supersedes:` themselves are still active;
|
|
62
|
+
* this helper does not currently follow supersession chains beyond raw count, which is
|
|
63
|
+
* deliberate — the curator reads the file directly to make the soft-archive plan.
|
|
64
|
+
*/
|
|
65
|
+
export declare function countActiveKnowledgeEntries(text: string): number;
|
|
51
66
|
export {};
|
package/dist/runs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { canTransition, createInitialFlowState } from "./flow-state.js";
|
|
4
|
+
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
|
|
5
5
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
6
6
|
export class InvalidStageTransitionError extends Error {
|
|
7
7
|
from;
|
|
@@ -156,8 +156,27 @@ function sanitizeStageGateCatalog(value, fallback) {
|
|
|
156
156
|
}
|
|
157
157
|
return next;
|
|
158
158
|
}
|
|
159
|
+
function coerceTrack(value) {
|
|
160
|
+
return isFlowTrack(value) ? value : "standard";
|
|
161
|
+
}
|
|
162
|
+
function sanitizeSkippedStages(value, track) {
|
|
163
|
+
const trackDefault = skippedStagesForTrack(track);
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
return trackDefault;
|
|
166
|
+
}
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const raw of value) {
|
|
170
|
+
if (isFlowStage(raw) && !seen.has(raw)) {
|
|
171
|
+
seen.add(raw);
|
|
172
|
+
out.push(raw);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out.length > 0 ? out : trackDefault;
|
|
176
|
+
}
|
|
159
177
|
function coerceFlowState(parsed) {
|
|
160
|
-
const
|
|
178
|
+
const track = coerceTrack(parsed.track);
|
|
179
|
+
const next = createInitialFlowState("active", track);
|
|
161
180
|
const activeRunIdRaw = parsed.activeRunId;
|
|
162
181
|
const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
|
|
163
182
|
? activeRunIdRaw.trim()
|
|
@@ -167,7 +186,9 @@ function coerceFlowState(parsed) {
|
|
|
167
186
|
currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
|
|
168
187
|
completedStages: sanitizeCompletedStages(parsed.completedStages),
|
|
169
188
|
guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
|
|
170
|
-
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog)
|
|
189
|
+
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
|
|
190
|
+
track,
|
|
191
|
+
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track)
|
|
171
192
|
};
|
|
172
193
|
}
|
|
173
194
|
function toArchiveDate(date = new Date()) {
|
|
@@ -373,12 +394,46 @@ export async function archiveRun(projectRoot, featureName) {
|
|
|
373
394
|
snapshottedStateFiles
|
|
374
395
|
};
|
|
375
396
|
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
397
|
+
const knowledgeStats = await readKnowledgeStats(projectRoot);
|
|
376
398
|
return {
|
|
377
399
|
archiveId,
|
|
378
400
|
archivePath,
|
|
379
401
|
archivedAt,
|
|
380
402
|
featureName: feature,
|
|
381
403
|
resetState,
|
|
382
|
-
snapshottedStateFiles
|
|
404
|
+
snapshottedStateFiles,
|
|
405
|
+
knowledge: knowledgeStats
|
|
383
406
|
};
|
|
384
407
|
}
|
|
408
|
+
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
409
|
+
async function readKnowledgeStats(projectRoot) {
|
|
410
|
+
const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.md");
|
|
411
|
+
let activeEntryCount = 0;
|
|
412
|
+
if (await exists(knowledgePath)) {
|
|
413
|
+
const text = await fs.readFile(knowledgePath, "utf8");
|
|
414
|
+
activeEntryCount = countActiveKnowledgeEntries(text);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
activeEntryCount,
|
|
418
|
+
softThreshold: KNOWLEDGE_SOFT_THRESHOLD,
|
|
419
|
+
overThreshold: activeEntryCount > KNOWLEDGE_SOFT_THRESHOLD,
|
|
420
|
+
knowledgePath: `${RUNTIME_ROOT}/knowledge.md`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Counts active (non-superseded) knowledge entries.
|
|
425
|
+
* An entry is a markdown H3 heading with the canonical timestamped format produced by
|
|
426
|
+
* `learn add` / `learn curate`. Entries marked `Supersedes:` themselves are still active;
|
|
427
|
+
* this helper does not currently follow supersession chains beyond raw count, which is
|
|
428
|
+
* deliberate — the curator reads the file directly to make the soft-archive plan.
|
|
429
|
+
*/
|
|
430
|
+
export function countActiveKnowledgeEntries(text) {
|
|
431
|
+
const lines = text.split(/\r?\n/);
|
|
432
|
+
let count = 0;
|
|
433
|
+
for (const line of lines) {
|
|
434
|
+
if (/^###\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\s+\[(rule|pattern|lesson|compound)\]/u.test(line)) {
|
|
435
|
+
count += 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return count;
|
|
439
|
+
}
|